Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(datasource): Add python-version datasource #27583

Merged
merged 45 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9aa07d5
feat(datasource): add python-version
not7cd Feb 27, 2024
454242f
Register Datasource
not7cd Feb 27, 2024
3351be5
Fix url
not7cd Feb 27, 2024
2a0f37e
Fix test
not7cd Feb 27, 2024
f8d3cba
Organize imports
not7cd Feb 27, 2024
c544d74
Skip uninstallable versions
not7cd Feb 27, 2024
8cac67f
Refactor to use zod schema
not7cd Feb 27, 2024
a635d42
remove debug
not7cd Feb 27, 2024
2428652
Merge branch 'main' into feat/python-version
not7cd Feb 27, 2024
7bfbc2f
Add datasource-python-version to cache
not7cd Feb 27, 2024
3a1f559
make eslint happy
not7cd Feb 27, 2024
54d2a4f
Formatting
not7cd Feb 27, 2024
445778a
Format release.json
not7cd Feb 27, 2024
deb3af9
format again
not7cd Feb 27, 2024
be5dfc7
Check for prebuild version
not7cd Feb 28, 2024
a13553a
python EOL
not7cd Feb 28, 2024
7922ba0
Add additional fixtures
not7cd Feb 29, 2024
92add60
Use Set<string>
not7cd Mar 20, 2024
feb1501
Test for endoflife-date datasource
not7cd Mar 29, 2024
fef20ea
Handle brebuilt versions
not7cd Mar 29, 2024
2d24ef0
Reduce fixture size
not7cd Mar 29, 2024
2984a0f
Reduce fixture even more
not7cd Mar 29, 2024
377e7f3
never undefined
not7cd Mar 29, 2024
044c1ea
implement getNewValue in python versioning
not7cd Apr 2, 2024
0469992
Update lib/modules/datasource/python-version/index.spec.ts
not7cd Apr 4, 2024
1c5aa73
Apply suggestions from code review
not7cd Apr 4, 2024
56ef7de
Merge branch 'main' into feat/python-version
not7cd Apr 6, 2024
e689a89
Update lib/modules/datasource/python-version/readme.md
not7cd Apr 8, 2024
e0f2b6c
Merge branch 'main' into feat/python-version
not7cd Apr 8, 2024
374e74a
Merge branch 'main' into feat/python-version
not7cd Apr 9, 2024
15f93e7
Merge branch 'main' into feat/python-version
rarkins Apr 18, 2024
41d3a00
instance properties
not7cd Apr 18, 2024
84b1dbe
remove type
not7cd Apr 18, 2024
7953fb8
update tests
not7cd Apr 18, 2024
0910ea5
Reduce fixtures even more
not7cd Apr 18, 2024
12202b4
Reduce even more
not7cd Apr 18, 2024
3a08def
Update readme
not7cd Apr 18, 2024
b56f24a
Update lib/modules/datasource/python-version/index.ts
not7cd Apr 18, 2024
3d247a7
Lint
not7cd Apr 18, 2024
04fbe9c
Apply suggestions from code review
not7cd Apr 18, 2024
965d95e
Update lib/modules/datasource/python-version/readme.md
not7cd Apr 19, 2024
916b641
Fix docs
not7cd Apr 20, 2024
dcd2f06
Use relative paths to concrete files
not7cd Apr 21, 2024
caba02b
Merge branch 'main' into feat/python-version
not7cd May 2, 2024
261dcd4
Merge branch 'main' into feat/python-version
rarkins May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/modules/datasource/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { PackagistDatasource } from './packagist';
import { PodDatasource } from './pod';
import { PuppetForgeDatasource } from './puppet-forge';
import { PypiDatasource } from './pypi';
import { PythonVersionDatasource } from './python-version';
import { RepologyDatasource } from './repology';
import { RubyVersionDatasource } from './ruby-version';
import { RubyGemsDatasource } from './rubygems';
Expand Down Expand Up @@ -120,6 +121,7 @@ api.set(PackagistDatasource.id, new PackagistDatasource());
api.set(PodDatasource.id, new PodDatasource());
api.set(PuppetForgeDatasource.id, new PuppetForgeDatasource());
api.set(PypiDatasource.id, new PypiDatasource());
api.set(PythonVersionDatasource.id, new PythonVersionDatasource());
api.set(RepologyDatasource.id, new RepologyDatasource());
api.set(RubyVersionDatasource.id, new RubyVersionDatasource());
api.set(RubyGemsDatasource.id, new RubyGemsDatasource());
Expand Down
4 changes: 4 additions & 0 deletions lib/modules/datasource/python-version/__fixtures__/eol.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
{"cycle":"3.12","releaseDate":"2023-10-02","support":"2025-04-02","eol":"2028-10-31","latest":"3.12.2","latestReleaseDate":"2024-02-06","lts":false},
{"cycle":"3.7","releaseDate":"2018-06-26","support":"2020-06-27","eol":"2023-06-27","latest":"3.7.17","latestReleaseDate":"2023-06-05","lts":false}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
not7cd marked this conversation as resolved.
Show resolved Hide resolved
{"name": "Python 3.12.0", "slug": "python-3120", "version": 3, "is_published": true, "is_latest": false, "release_date": "2023-10-02T12:50:09Z", "pre_release": false, "release_page": null, "release_notes_url": "https://docs.python.org/release/3.12.0/whatsnew/changelog.html#python-3-12-0", "show_on_download_page": true, "resource_uri": "https://www.python.org/api/v2/downloads/release/832/"},
{"name": "Python 3.12.0a1", "slug": "python-3120a1", "version": 3, "is_published": true, "is_latest": false, "release_date": "2022-10-25T02:16:12Z", "pre_release": true, "release_page": null, "release_notes_url": "", "show_on_download_page": false, "resource_uri": "https://www.python.org/api/v2/downloads/release/767/"},
{"name": "Python 3.12.2", "slug": "python-3122", "version": 3, "is_published": true, "is_latest": true, "release_date": "2024-02-06T21:40:35Z", "pre_release": false, "release_page": null, "release_notes_url": "https://docs.python.org/release/3.12.2/whatsnew/changelog.html#python-3-12-2", "show_on_download_page": true, "resource_uri": "https://www.python.org/api/v2/downloads/release/871/"},
{"name": "Python 3.7.8", "slug": "python-378", "version": 3, "is_published": true, "is_latest": false, "release_date": "2020-06-27T12:55:01Z", "pre_release": false, "release_page": null, "release_notes_url": "https://docs.python.org/release/3.7.8/whatsnew/changelog.html#changelog", "show_on_download_page": true, "resource_uri": "https://www.python.org/api/v2/downloads/release/442/"},
{"name": "Python 3.7.9", "slug": "python-379", "version": 3, "is_published": true, "is_latest": false, "release_date": "2020-08-17T22:00:00Z", "pre_release": false, "release_page": null, "release_notes_url": "https://docs.python.org/release/3.7.9/whatsnew/changelog.html#changelog", "show_on_download_page": true, "resource_uri": "https://www.python.org/api/v2/downloads/release/482/"}
]
5 changes: 5 additions & 0 deletions lib/modules/datasource/python-version/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const defaultRegistryUrl =
'https://www.python.org/api/v2/downloads/release';
export const githubBaseUrl = 'https://api.github.com/';

export const datasource = 'python-version';
148 changes: 148 additions & 0 deletions lib/modules/datasource/python-version/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { satisfies } from '@renovatebot/pep440';
import { getPkgReleases } from '..';
import { Fixtures } from '../../../../test/fixtures';
import * as httpMock from '../../../../test/http-mock';
import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages';
import * as githubGraphql from '../../../util/github/graphql';
import { registryUrl as eolRegistryUrl } from '../endoflife-date/common';
import { datasource, defaultRegistryUrl } from './common';
import { PythonVersionDatasource } from '.';

describe('modules/datasource/python-version/index', () => {
describe('dependent datasources', () => {
it('returns Python EOL data', async () => {
const datasource = new PythonVersionDatasource();
httpMock
.scope(eolRegistryUrl)
.get('/python.json')
.reply(200, Fixtures.get('eol.json'));
const res = await datasource.getEolReleases();
expect(
res?.releases.find((release) => release.version === '3.7.17')
?.isDeprecated,
).toBeTrue();
});
});

describe('getReleases', () => {
beforeEach(() => {
httpMock
.scope('https://endoflife.date')
.get('/api/python.json')
.reply(200, Fixtures.get('eol.json'));

jest.spyOn(githubGraphql, 'queryReleases').mockResolvedValueOnce([
{
id: 1,
url: 'https://example.com',
name: 'containerbase/python-prebuild',
description: 'some description',
version: '3.12.1',
releaseTimestamp: '2020-03-09T13:00:00Z',
},
{
id: 2,
url: 'https://example.com',
name: 'containerbase/python-prebuild',
description: 'some description',
version: '3.12.0',
releaseTimestamp: '2020-03-09T13:00:00Z',
},
{
id: 3,
url: 'https://example.com',
name: 'containerbase/python-prebuild',
description: 'some description',
version: '3.7.8',
releaseTimestamp: '2020-03-09T13:00:00Z',
},
]);
});

it('throws for 500', async () => {
httpMock.scope(defaultRegistryUrl).get('').reply(500);
await expect(
not7cd marked this conversation as resolved.
Show resolved Hide resolved
getPkgReleases({
datasource,
packageName: 'python',
}),
).rejects.toThrow(EXTERNAL_HOST_ERROR);
});

it('returns null for error', async () => {
httpMock.scope(defaultRegistryUrl).get('').replyWithError('error');
expect(
await getPkgReleases({
datasource,
packageName: 'python',
}),
).toBeNull();
});

it('returns null for empty 200 OK', async () => {
httpMock.scope(defaultRegistryUrl).get('').reply(200, []);
expect(
await getPkgReleases({
datasource,
packageName: 'python',
}),
).toBeNull();
});

describe('processes real data', () => {
beforeEach(() => {
httpMock
.scope(defaultRegistryUrl)
.get('')
.reply(200, Fixtures.get('release.json'));
});

it('returns the correct data', async () => {
const res = await getPkgReleases({
datasource,
packageName: 'python',
});
expect(res?.releases[0]).toEqual({
not7cd marked this conversation as resolved.
Show resolved Hide resolved
isDeprecated: true,
isStable: true,
releaseTimestamp: '2020-06-27T12:55:01.000Z',
version: '3.7.8',
});
});

it('only returns stable versions', async () => {
const res = await getPkgReleases({
datasource,
packageName: 'python',
});
expect(res?.releases).toHaveLength(2);
for (const release of res?.releases ?? []) {
expect(release.isStable).toBeTrue();
}
});

it('only returns versions that are prebuilt', async () => {
const res = await getPkgReleases({
datasource,
packageName: 'python',
});
expect(
res?.releases.filter((release) =>
satisfies(release.version, '>3.12.1'),
),
).toHaveLength(0);
});

it('returns isDeprecated status for Python 3 minor releases', async () => {
const res = await getPkgReleases({
datasource,
packageName: 'python',
});
expect(res?.releases).toHaveLength(2);
for (const release of res?.releases ?? []) {
expect(release.isDeprecated).toBeBoolean();
}
});
not7cd marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
92 changes: 92 additions & 0 deletions lib/modules/datasource/python-version/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { cache } from '../../../util/cache/package/decorator';
import { id as versioning } from '../../versioning/python';
import { Datasource } from '../datasource';
import { EndoflifeDatePackagesource } from '../endoflife-date';
import { registryUrl as eolRegistryUrl } from '../endoflife-date/common';
import { GithubReleasesDatasource } from '../github-releases';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import { datasource, defaultRegistryUrl, githubBaseUrl } from './common';
import { PythonRelease } from './schema';

export class PythonVersionDatasource extends Datasource {
static readonly id = datasource;
pythonPrebuildDatasource: GithubReleasesDatasource;
pythonEolDatasource: EndoflifeDatePackagesource;

constructor() {
super(datasource);
this.pythonPrebuildDatasource = new GithubReleasesDatasource();
this.pythonEolDatasource = new EndoflifeDatePackagesource();
}

override readonly customRegistrySupport = false;

override readonly defaultRegistryUrls = [defaultRegistryUrl];

override readonly defaultVersioning = versioning;

override readonly caching = true;

async getPrebuildReleases(): Promise<ReleaseResult | null> {
return await this.pythonPrebuildDatasource.getReleases({
registryUrl: githubBaseUrl,
packageName: 'containerbase/python-prebuild',
});
}

async getEolReleases(): Promise<ReleaseResult | null> {
return await this.pythonEolDatasource.getReleases({
registryUrl: eolRegistryUrl,
packageName: 'python',
});
}

@cache({
namespace: `datasource-${datasource}`,
key: ({ registryUrl }: GetReleasesConfig) => `${registryUrl}`,
})
async getReleases({
registryUrl,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
// istanbul ignore if
if (!registryUrl) {
return null;
}
const pythonPrebuildReleases = await this.getPrebuildReleases();
const pythonPrebuildVersions = new Set<string>(
pythonPrebuildReleases?.releases.map((release) => release.version),
);
const pythonEolReleases = await this.getEolReleases();
const pythonEolVersions = new Map(
pythonEolReleases?.releases
.filter((release) => release.isDeprecated !== undefined)
.map((release) => [
release.version.split('.').slice(0, 2).join('.'),
release.isDeprecated,
]),
);
const result: ReleaseResult = {
homepage: 'https://python.org',
sourceUrl: 'https://github.com/python/cpython',
registryUrl,
releases: [],
};
try {
const response = await this.http.getJson(registryUrl, PythonRelease);
result.releases.push(
...response.body
.filter((release) => release.isStable)
.filter((release) => pythonPrebuildVersions.has(release.version)),
);
} catch (err) {
this.handleGenericErrors(err);
}
for (const release of result.releases) {
release.isDeprecated = pythonEolVersions.get(
release.version.split('.').slice(0, 2).join('.'),
);
}

return result.releases.length ? result : null;
}
}
32 changes: 32 additions & 0 deletions lib/modules/datasource/python-version/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
This datasource returns Python releases from the [python.org API](https://www.python.org/api/v2/downloads/release/).
not7cd marked this conversation as resolved.
Show resolved Hide resolved

It also fetches deprecated versions from the [Endoflife Date datasource](/modules/datasource/endoflife-date/).
not7cd marked this conversation as resolved.
Show resolved Hide resolved

Because Renovate depends on [`containerbase/python-prebuild`](https://github.com/containerbase/python-prebuild/releases) it will also fetch releases from the GitHub API.

## Example custom manager

Below is a [custom regex manager](/modules/manager/regex/) to update the Python versions in a Dockerfile.
not7cd marked this conversation as resolved.
Show resolved Hide resolved
Python versions sometimes drop the dot that separate the major and minor number: so `3.11` becomes `311`.
The example below handles this case.

```dockerfile
ARG PYTHON_VERSION=311
FROM image-python${PYTHON_VERSION}-builder:1.0.0
```

```json
"customManagers": [
{
"customType": "regex",
"fileMatch": ["^Dockerfile$"],
"matchStringsStrategy": "any",
"matchStrings": ["ARG PYTHON_VERSION=\"?(?<currentValue>3(?<minor>\\d+))\"?\\s"],
"autoReplaceStringTemplate": "ARG PYTHON_VERSION={{{replace '\\.' '' newValue}}}\n",
"currentValueTemplate": "3.{{{minor}}}",
"datasourceTemplate": "python-version",
"versioningTemplate": "python",
"depNameTemplate": "python"
}
]
```
not7cd marked this conversation as resolved.
Show resolved Hide resolved
31 changes: 31 additions & 0 deletions lib/modules/datasource/python-version/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from 'zod';
import type { Release } from '../types';

export const PythonRelease = z
.object({
/** e.g: "Python 3.9.0b1" */
name: z.string(),
/** e.g: "python-390b1" */
slug: z.string(),
/** Major version e.g: 3 */
version: z.number(),
/** is latest major version, true for Python 2.7.18 and latest Python 3 */
is_latest: z.boolean(),
is_published: z.boolean(),
release_date: z.string(),
pre_release: z.boolean(),
release_page: z.string().nullable(),
show_on_download_page: z.boolean(),
/** Changelog e.g: "https://docs.python.org/…html#python-3-9-0-beta-1" */
release_notes_url: z.string(),
/** Download URL e.g: "https://www.python.org/api/v2/downloads/release/436/" */
resource_uri: z.string(),
})
.transform(
({ name, release_date: releaseTimestamp, pre_release }): Release => {
const version = name?.replace('Python', '').trim();
const isStable = pre_release === false;
return { version, releaseTimestamp, isStable };
},
)
.array();
1 change: 1 addition & 0 deletions lib/util/cache/package/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type PackageCacheNamespace =
| 'datasource-packagist-public-files'
| 'datasource-packagist'
| 'datasource-pod'
| 'datasource-python-version'
| 'datasource-releases'
| 'datasource-repology-list'
| 'datasource-ruby-version'
Expand Down
Loading