diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 68a36675d..621563ce9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,13 +6,27 @@ jobs: build: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository runs-on: ubuntu-latest + env: + PAT: ${{ secrets.PAT }} steps: - uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT || github.token }} - uses: actions/setup-python@v5 with: python-version: '3.10' cache: pip cache-dependency-path: '**/requirements*.txt' + - id: changed-files + uses: tj-actions/changed-files@v45 + - uses: pre-commit/action@v3.0.1 + continue-on-error: true + with: + extra_args: pip-compile --files ${{ steps.changed-files.outputs.all_changed_files }} + - if: ${{ env.PAT }} + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: '[github-actions] pre-commit autoupdate' - shell: bash run: curl -s -S --retry 3 $BASEDIR/tests/install.sh | bash - - shell: bash diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..8c81d5c10 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +ci: + autoupdate_schedule: quarterly + skip: [pip-compile] +default_language_version: + python: python3.10 +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.9 + hooks: + - id: ruff + - id: ruff-format + - repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.4.18 + hooks: + - id: pip-compile + name: pip-compile common-requirements.in + args: [common-requirements.in, -o, common-requirements.txt] + files: ^common-requirements\.(in|txt)$ diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..c8cfe3959 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/common-requirements.in b/common-requirements.in index 9832fc75d..bc75f3234 100644 --- a/common-requirements.in +++ b/common-requirements.in @@ -2,7 +2,7 @@ linkify-it-py myst-parser ocds-babel Sphinx --e git+https://github.com/open-contracting/standard_theme.git@open_contracting#egg=standard_theme +git+https://github.com/open-contracting/standard_theme.git@open_contracting#egg=standard_theme # Profile ocdsextensionregistry @@ -19,5 +19,4 @@ selenium # Development click -pip-tools sphinx-autobuild diff --git a/common-requirements.txt b/common-requirements.txt index ff4de52f3..9dd5cffd2 100644 --- a/common-requirements.txt +++ b/common-requirements.txt @@ -1,13 +1,11 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile common-requirements.in -# --e git+https://github.com/open-contracting/standard_theme.git@open_contracting#egg=standard_theme - # via -r common-requirements.in +# This file was autogenerated by uv via the following command: +# uv pip compile common-requirements.in -o common-requirements.txt alabaster==0.7.12 # via sphinx +anyio==4.4.0 + # via + # starlette + # watchfiles async-generator==1.10 # via # trio @@ -23,11 +21,9 @@ babel==2.9.1 # via # sphinx # sphinx-intl -build==0.10.0 - # via pip-tools cattrs==23.1.2 # via requests-cache -certifi==2023.7.22 +certifi==2024.7.4 # via # elastic-transport # requests @@ -38,9 +34,9 @@ click==8.1.3 # via # -r common-requirements.in # ocdsindex - # pip-tools # sphinx-intl -colorama==0.4.4 + # uvicorn +colorama==0.4.6 # via sphinx-autobuild docutils==0.18 # via @@ -48,16 +44,20 @@ docutils==0.18 # sphinx elastic-transport==8.4.0 # via elasticsearch -elasticsearch[requests]==8.6.2 +elasticsearch==8.6.2 # via ocdsindex -exceptiongroup==1.0.0 +exceptiongroup==1.2.2 # via + # anyio # cattrs # pytest h11==0.13.0 - # via wsproto + # via + # uvicorn + # wsproto idna==3.7 # via + # anyio # requests # trio imagesize==1.4.1 @@ -74,8 +74,6 @@ jsonref==1.0.0.post1 # via ocdsextensionregistry linkify-it-py==1.0.1 # via -r common-requirements.in -livereload==2.6.3 - # via sphinx-autobuild lxml==4.9.1 # via ocdsindex markdown-it-py==2.2.0 @@ -90,31 +88,24 @@ mdurl==0.1.2 # via markdown-it-py myst-parser==0.18.1 # via -r common-requirements.in -ocds-babel==0.3.1 +ocds-babel==0.3.6 # via -r common-requirements.in -ocdsextensionregistry==0.3.8 +ocdsextensionregistry==0.5.0 # via -r common-requirements.in ocdsindex==0.2.0 # via -r common-requirements.in outcome==1.1.0 # via trio -packaging==21.3 +packaging==24.1 # via - # build # pytest # sphinx -pip-tools==7.3.0 - # via -r common-requirements.in platformdirs==3.9.1 # via requests-cache pluggy==0.13.1 # via pytest pygments==2.15.1 # via sphinx -pyparsing==2.4.7 - # via packaging -pyproject-hooks==1.0.0 - # via build pysocks==1.7.1 # via urllib3 pytest==7.2.0 @@ -133,12 +124,14 @@ requests-cache==1.1.0 # via ocdsextensionregistry selenium==4.11.2 # via -r common-requirements.in +setuptools==75.2.0 + # via sphinx-intl six==1.16.0 - # via - # livereload - # url-normalize + # via url-normalize sniffio==1.2.0 - # via trio + # via + # anyio + # trio snowballstemmer==2.1.0 # via sphinx sortedcontainers==2.4.0 @@ -149,7 +142,7 @@ sphinx==5.3.0 # myst-parser # sphinx-autobuild # sphinx-intl -sphinx-autobuild==2021.3.14 +sphinx-autobuild==2024.9.3 # via -r common-requirements.in sphinx-intl==2.2.0 # via -r common-requirements.in @@ -165,14 +158,12 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx +standard-theme @ git+https://github.com/open-contracting/standard_theme.git@07ca0e39979a244656dd6df0658f2ead428184b9#egg=standard_theme + # via -r common-requirements.in +starlette==0.40.0 + # via sphinx-autobuild tomli==2.0.1 - # via - # build - # pip-tools - # pyproject-hooks - # pytest -tornado==6.4.1 - # via livereload + # via pytest trio==0.20.0 # via # selenium @@ -181,23 +172,25 @@ trio-websocket==0.9.2 # via selenium typing-extensions==4.4.0 # via + # anyio # cattrs # myst-parser + # uvicorn uc-micro-py==1.0.1 # via linkify-it-py url-normalize==1.4.3 # via requests-cache -urllib3[socks]==1.26.18 +urllib3==1.26.19 # via # elastic-transport # requests # requests-cache # selenium -wheel==0.38.4 - # via pip-tools +uvicorn==0.30.6 + # via sphinx-autobuild +watchfiles==0.24.0 + # via sphinx-autobuild +websockets==13.0.1 + # via sphinx-autobuild wsproto==1.1.0 # via trio-websocket - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools diff --git a/docs/conf.py b/docs/conf.py index 46f6ddbd4..5f8d438c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,10 +9,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) + import csv import json import os diff --git a/docs/history/changelog.md b/docs/history/changelog.md index b3089ac9e..a717f9dc2 100644 --- a/docs/history/changelog.md +++ b/docs/history/changelog.md @@ -348,7 +348,7 @@ Per the [normative and non-normative content and changes policy](../governance/n * Identifiers * [#1094](https://github.com/open-contracting/standard/pull/1094) Add guidance on populating `parties.id` for parties without an organization identifier. * [#1643](https://github.com/open-contracting/standard/pull/1643) Update identifier section in release reference. - * [#1655](https://github.com/open-contracting/standard/pull/1655) Rewrite identifiers reference and examples for clarity. + * [#1655](https://github.com/open-contracting/standard/pull/1655), [#1708](https://github.com/open-contracting/standard/pull/1708) Rewrite identifiers reference and examples for clarity. * Documents * [#1189](https://github.com/open-contracting/standard/pull/1189) Add recommendations about publishing and referencing documents in the document reference section. * [#1664](https://github.com/open-contracting/standard/pull/1664) Recommend linking to alternative representations using `documents`. diff --git a/docs/schema/identifiers.md b/docs/schema/identifiers.md index 211a37611..19b06dd1b 100644 --- a/docs/schema/identifiers.md +++ b/docs/schema/identifiers.md @@ -79,6 +79,14 @@ The only purpose of the OCID prefix is to turn *locally* unique identifiers into To ensure that your `ocid`s do not conflict with those of another publisher, you must [register an OCID prefix](../guidance/build.md#register-an-ocid-prefix). +```{admonition} Who can register an OCID prefix? +:class: hint + +In principle, anyone can register an OCID prefix. In practice, it is primarily governments and public institutions, as well as businesses and civil society organizations. + +There can be multiple government publishers in a jurisdiction. For example, if the national government centralizes data about contracting processes, then it might be the only government to publish in that jurisdiction, using a single OCID prefix. On the other hand, if subnational governments have the authority to procure, control procurement systems, and/or collect procurement data, then they might also publish, using their own OCID prefixes. +``` + Only the publisher that registered an OCID prefix is authorized to assign new `ocid`s with that OCID prefix, or to delegate this responsibility. ```{note} diff --git a/manage.py b/manage.py index 117fa426d..b543580d9 100755 --- a/manage.py +++ b/manage.py @@ -31,7 +31,7 @@ sys.path.append(str(basedir / 'docs')) -from conf import release # noqa isort:skip +from conf import release # noqa: E402 def custom_warning_formatter(message, category, filename, lineno, line=None): @@ -40,7 +40,7 @@ def custom_warning_formatter(message, category, filename, lineno, line=None): warnings.formatwarning = custom_warning_formatter -versioned_template = json.loads(''' +versioned_template = json.loads(""" { "type": "array", "items": { @@ -63,7 +63,7 @@ def custom_warning_formatter(message, category, filename, lineno, line=None): } } } -''') +""") common_versioned_definitions = { 'StringNullUriVersioned': { @@ -118,35 +118,26 @@ def custom_warning_formatter(message, category, filename, lineno, line=None): def json_load(filename, library=json, **kwargs): - """ - Loads JSON data from the given filename. - """ + """Load JSON data from the given filename.""" with (schemadir / filename).open() as f: return library.load(f, **kwargs) def json_dump(filename, data): - """ - Writes JSON data to the given filename. - """ + """Write JSON data to the given filename.""" with (schemadir / filename).open('w') as f: json.dump(data, f, indent=2) f.write('\n') def csv_load(url, delimiter=','): - """ - Loads CSV data into a ``csv.DictReader`` from the given URL. - """ - reader = csv.DictReader(StringIO(get(url).text), delimiter=delimiter) - return reader + """Load CSV data into a ``csv.DictReader`` from the given URL.""" + return csv.DictReader(StringIO(get(url).text), delimiter=delimiter) @contextmanager def csv_dump(filename, fieldnames): - """ - Writes CSV headers to the given filename, and yields a ``csv.writer``. - """ + """Write CSV headers to the given filename, and yield a ``csv.writer``.""" f = (schemadir / 'codelists' / filename).open('w') writer = csv.writer(f, lineterminator='\n') writer.writerow(fieldnames) @@ -157,18 +148,14 @@ def csv_dump(filename, fieldnames): def get(url): - """ - GETs a URL and returns the response. Raises an exception if the status code is not successful. - """ - response = requests.get(url) + """GET a URL and returns the response. Raise an exception if the status code is not successful.""" + response = requests.get(url, timeout=10) response.raise_for_status() return response def coerce_to_list(data, key): - """ - Returns the value of the ``key`` key in the ``data`` mapping. If the value is a string, wraps it in an array. - """ + """Return the value of the ``key`` key in the ``data`` mapping. If the value is a string, wrap it in an array.""" item = data.get(key, []) if isinstance(item, str): return [item] @@ -176,16 +163,14 @@ def coerce_to_list(data, key): def get_metaschema(): - """ - Patches and returns the JSON Schema Draft 4 metaschema. - """ + """Patches and returns the JSON Schema Draft 4 metaschema.""" return json_merge_patch.merge(json_load('metaschema/json-schema-draft-4.json'), json_load('metaschema/meta-schema-patch.json')) def get_common_definition_ref(item): """ - Returns a schema that references the common definition that the ``item`` matches: "StringNullUriVersioned", + Return a schema that references the common definition that the ``item`` matches: "StringNullUriVersioned", "StringNullDateTimeVersioned" or "StringNullVersioned". """ for name, keywords in common_versioned_definitions.items(): @@ -193,15 +178,14 @@ def get_common_definition_ref(item): if any(item.get(keyword) != value for keyword, value in keywords.items()): continue # And adds no keywords to the definition. - if any(keyword not in (*keywords, *keywords_to_remove) for keyword in item): + if any(keyword not in {*keywords, *keywords_to_remove} for keyword in item): continue return {'$ref': f'#/definitions/{name}'} + return None def add_versioned(schema, unversioned_pointers, pointer=''): - """ - An outer function that calls ``_add_versioned`` on each field. - """ + """Call ``_add_versioned`` on each field.""" for key, value in schema['properties'].items(): new_pointer = f'{pointer}/properties/{key}' _add_versioned(schema, unversioned_pointers, new_pointer, key, value) @@ -213,7 +197,7 @@ def add_versioned(schema, unversioned_pointers, pointer=''): def _add_versioned(schema, unversioned_pointers, pointer, key, value): """ - An inner function that performs the changes to the schema to refer to versioned/unversioned definitions. + Perform the changes to the schema to refer to versioned/unversioned definitions. :param schema dict: the schema of the object on which the field is defined :param unversioned_pointers set: JSON Pointers to ``id`` fields to leave unversioned if the object is in an array @@ -286,9 +270,7 @@ def _add_versioned(schema, unversioned_pointers, pointer, key, value): def update_refs_to_unversioned_definitions(schema): - """ - Replaces ``$ref`` values with unversioned definitions. - """ + """Replace ``$ref`` values with unversioned definitions.""" for key, value in schema.items(): if key == '$ref': schema[key] = value + 'Unversioned' @@ -297,9 +279,7 @@ def update_refs_to_unversioned_definitions(schema): def get_unversioned_pointers(schema, fields, pointer=''): - """ - Returns the JSON Pointers to ``id`` fields that must not be versioned if the object is in an array. - """ + """Return the JSON Pointers to ``id`` fields that must not be versioned if the object is in an array.""" if isinstance(schema, list): for index, item in enumerate(schema): get_unversioned_pointers(item, fields, pointer=f'{pointer}/{index}') @@ -328,9 +308,7 @@ def get_unversioned_pointers(schema, fields, pointer=''): def remove_omit_when_merged(schema): - """ - Removes properties that set ``omitWhenMerged``. - """ + """Remove properties that set ``omitWhenMerged``.""" if isinstance(schema, list): for item in schema: remove_omit_when_merged(item) @@ -346,15 +324,13 @@ def remove_omit_when_merged(schema): def remove_metadata_and_extended_keywords(schema): - """ - Removes metadata and extended keywords from properties and definitions. - """ + """Remove metadata and extended keywords from properties and definitions.""" if isinstance(schema, list): for item in schema: remove_metadata_and_extended_keywords(item) elif isinstance(schema, dict): for key, value in schema.items(): - if key in ('definitions', 'properties'): + if key in {'definitions', 'properties'}: for subschema in value.values(): for keyword in keywords_to_remove: subschema.pop(keyword, None) @@ -362,12 +338,10 @@ def remove_metadata_and_extended_keywords(schema): def get_versioned_release_schema(schema): - """ - Returns the versioned release schema. - """ + """Return the versioned release schema.""" # Update schema metadata. release_with_underscores = release.replace('.', '__') - schema['id'] = f'https://standard.open-contracting.org/schema/{release_with_underscores}/versioned-release-validation-schema.json' # noqa: E501 + schema['id'] = f'https://standard.open-contracting.org/schema/{release_with_underscores}/versioned-release-validation-schema.json' schema['title'] = 'Schema for a compiled, versioned Open Contracting Release.' # Release IDs, dates and tags appear alongside values in the versioned release schema. @@ -453,10 +427,7 @@ def unused_terms(filename): @cli.command() @click.option('--ignore-base', help='A base branch to ignore, e.g. 1.2-dev') def missing_changelog(ignore_base): - """ - Print pull requests not mentioned in the changelog. - """ - + """Print pull requests not mentioned in the changelog.""" # Ignore PRs to the ppp-extension branch, which became OCDS for PPPs. ignore = ['ppp-extension'] if ignore_base: @@ -497,7 +468,7 @@ def missing_changelog(ignore_base): base_ref = pr['base']['ref'] # Include merged PRs, not in the "Minor:" or "1.0-RC" milestones, not syncing branches, and not ignored. - if not merged_at or milestone_number in (26, 27, 28, 29, 2) or pattern.search(title) or base_ref in ignore: + if not merged_at or milestone_number in {26, 27, 28, 29, 2} or pattern.search(title) or base_ref in ignore: if number in prs: click.echo(f'WARNING: #{number} should not be in changelog', file=sys.stderr) continue @@ -558,9 +529,13 @@ def pre_commit(): if not multilingual and field.schema['type'] == 'object': click.secho(f'{field.path} is an object. {" & ".join(counts[name])} is/are multilingual.', fg='yellow') elif multilingual: - raise Exception(f'{name} is multilingual at {field.path}, but not elsewhere') + raise click.ClickException( + f'{name} is multilingual at {field.path}, but not elsewhere' + ) else: - raise Exception(f'{name} is multilingual at {" & ".join(counts[name])}, but not at {field.path}') + raise click.ClickException( + f'{name} is multilingual at {" & ".join(counts[name])}, but not at {field.path}' + ) if multilingual: counts[name].append(field.path) else: @@ -614,7 +589,8 @@ def update_country(file): # Clean "Western Sahara*", "United Arab Emirates (the)", etc. codes[d[str(offset + 9)]] = re.sub(r' \(the\)|\*', '', d[str(offset + 13)]) # The country code appears at offsets 9 and 15. Check that they are always the same. - assert d[str(offset + 9)] == d[str(offset + 15)] + if d[str(offset + 9)] != d[str(offset + 15)]: + raise AssertionError with open(schemadir / 'codelists' / 'country.csv', 'w') as f: writer = csv.writer(f, lineterminator='\n') @@ -625,16 +601,14 @@ def update_country(file): @cli.command() def update_currency(): - """ - Update currency.csv from ISO 4217. - """ + """Update currency.csv from ISO 4217.""" # https://www.iso.org/iso-4217-currency-codes.html # https://www.six-group.com/en/products-services/financial-information/data-standards.html#scrollTo=currency-codes - # "List One: Current Currency & Funds" + # List One: Current Currency & Funds current_codes = {} - url = 'https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/amendments/lists/list_one.xml' # noqa: E501 - tree = etree.fromstring(get(url).content) + url = 'https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/amendments/lists/list_one.xml' + tree = etree.fromstring(get(url).content) # noqa: S320 # trusted external for node in tree.xpath('//CcyNtry'): # Entries like Antarctica have no universal currency. if node.xpath('./Ccy'): @@ -644,23 +618,23 @@ def update_currency(): current_codes[code] = title # We should expect currency titles to be consistent across countries. elif current_codes[code] != title: - raise Exception(f'expected {current_codes[code]}, got {title}') + raise click.ClickException(f'expected {current_codes[code]}, got {title}') - # "List Three: Historic Denominations (Currencies & Funds)" + # List Three: Historic Denominations (Currencies & Funds) historic_codes = {} - url = 'https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/amendments/lists/list_three.xml' # noqa: E501 - tree = etree.fromstring(get(url).content) + url = 'https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/amendments/lists/list_three.xml' + tree = etree.fromstring(get(url).content) # noqa: S320 # trusted external for node in tree.xpath('//HstrcCcyNtry'): code = node.xpath('./Ccy')[0].text title = node.xpath('./CcyNm')[0].text.strip() valid_until = node.xpath('./WthdrwlDt')[0].text # Use ISO8601 interval notation. valid_until = re.sub(r'^(\d{4})-(\d{4})$', r'\1/\2', valid_until.replace(' to ', '/')) - if code not in current_codes: - if code not in historic_codes: - historic_codes[code] = {'Title': title, 'Valid Until': valid_until} - # If the code is historical, use the most recent title and valid date. - elif valid_until > historic_codes[code]['Valid Until']: + if ( + code not in current_codes + # Last condition: If the code is historical, use the most recent title and valid date. + and (code not in historic_codes or valid_until > historic_codes[code]['Valid Until']) + ): historic_codes[code] = {'Title': title, 'Valid Until': valid_until} with csv_dump('currency.csv', ['Code', 'Title', 'Valid Until']) as writer: @@ -670,23 +644,20 @@ def update_currency(): writer.writerow([code, historic_codes[code]['Title'], historic_codes[code]['Valid Until']]) release_schema = json_load('release-schema.json') - codes = sorted(list(current_codes) + list(historic_codes)) - release_schema['definitions']['Value']['properties']['currency']['enum'] = codes + [None] + codes = sorted([*current_codes, historic_codes]) + release_schema['definitions']['Value']['properties']['currency']['enum'] = [*codes, None] json_dump('release-schema.json', release_schema) @cli.command() def update_language(): - """ - Update language.csv from ISO 639-1. - """ + """Update language.csv from ISO 639-1.""" # https://www.iso.org/iso-639-language-codes.html # https://id.loc.gov/vocabulary/iso639-1.html with csv_dump('language.csv', ['Code', 'Title']) as writer: - reader = csv_load('https://id.loc.gov/vocabulary/iso639-1.tsv', delimiter='\t') - for row in reader: + for row in csv_load('https://id.loc.gov/vocabulary/iso639-1.tsv', delimiter='\t'): # Remove parentheses, like "Greek, Modern (1453-)", and split alternatives. titles = re.split(r' *\| *', re.sub(r' \(.+\)', '', row['Label (English)'])) # Remove duplication like "Ndebele, North | North Ndebele" and join alternatives using a comma instead of @@ -720,8 +691,7 @@ def update_media_type(): with csv_dump('mediaType.csv', ['Code', 'Title']) as writer: for registry in registries: # See "Available Formats" under each heading. - reader = csv_load(f'https://www.iana.org/assignments/media-types/{registry}.csv') - for row in reader: + for row in csv_load(f'https://www.iana.org/assignments/media-types/{registry}.csv'): if ' ' in row['Name']: name, message = row['Name'].split(' ', 1) else: @@ -733,7 +703,7 @@ def update_media_type(): logging.warning('%s: %s', message, code) # "x-emf" has "image/emf" in its "Template" value (but it is deprecated). elif template and template != code: - raise Exception(f"expected {code}, got {template}") + raise click.ClickException(f"expected {code}, got {template}") else: writer.writerow([code, name]) @@ -743,9 +713,7 @@ def update_media_type(): @cli.command() @click.pass_context def update(ctx): - """ - Update codelists except country.csv. - """ + """Update codelists except country.csv.""" ctx.invoke(update_currency) ctx.invoke(update_language) ctx.invoke(update_media_type) @@ -797,9 +765,7 @@ def text(node, xpath): def add_translation_note(path, language, domain): - """ - Adds a translation note to a file. - """ + """Add a translation note to a file.""" base_url = 'https://standard.open-contracting.org/1.1' with open(path) as f: @@ -809,10 +775,10 @@ def add_translation_note(path, language, domain): _ = translator.gettext pattern = f'{base_url}/{{}}/{domain}/' - response = requests.get(pattern.format(language)) + response = requests.get(pattern.format(language), timeout=10) # If it's a new page, add the note to the current version of the page. - if response.status_code == 404: + if response.status_code == requests.codes.not_found: message = _('This page was recently added to the English documentation. ' 'It has not yet been translated.') @@ -832,11 +798,15 @@ def add_translation_note(path, language, domain): element = document.xpath(xpath)[0] element.getparent().replace(element, replacement) - message = _('This page was recently changed in the English documentation. ' - 'The changes have not yet been translated.') + message = _( + 'This page was recently changed in the English documentation. ' + 'The changes have not yet been translated.' + ) - template = '
%(note)s
' \ - '%(message)s
%(note)s
' + '%(message)s