From 665af429627ab4e277bc8ec177625cb2b9d64edc Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Tue, 22 Aug 2023 10:26:38 -0400
Subject: [PATCH 01/47] build: Update to latest profile template
(cherry picked from commit 914f1c1b44c50d1850d9495378b553363a2a3775)
---
.github/workflows/ci.yml | 5 +---
.gitignore | 4 ----
common-requirements.txt | 52 +++++++++++++++++++++-------------------
include/config.mk | 4 +++-
4 files changed, 32 insertions(+), 33 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 34f17b27b..ac16da1c8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,7 +10,7 @@ jobs:
submodules: true
- uses: actions/setup-python@v4
with:
- python-version: 3.8
+ python-version: '3.10'
cache: pip
cache-dependency-path: '**/requirements*.txt'
# Don't install editable projects in the current working directory.
@@ -23,9 +23,6 @@ jobs:
# - run: |
# make linkcheck
# rm -f output.json output.txt
- # Google Chrome and ChromeDriver are already included.
- # https://help.github.com/en/actions/reference/software-installed-on-github-hosted-runners
- # https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md
- run: pytest -W error
# Deploy the built documentation to the staging directory.
- if: github.event_name == 'push'
diff --git a/.gitignore b/.gitignore
index 16ebca1ad..2a40f9487 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,10 +6,6 @@
/*.pdf
/build
/cache.sqlite
-/chromedriver
-/chromedriver_linux64.zip
-/chromedriver_mac64.zip
-/chromedriver_win32.zip
/docs/_static/patched
/src
*.pyc
diff --git a/common-requirements.txt b/common-requirements.txt
index 4e586605b..7780d73bc 100644
--- a/common-requirements.txt
+++ b/common-requirements.txt
@@ -12,10 +12,12 @@ async-generator==1.10
# via
# trio
# trio-websocket
-attrs==20.3.0
+attrs==23.1.0
# via
+ # cattrs
# outcome
# pytest
+ # requests-cache
# trio
babel==2.9.1
# via
@@ -23,13 +25,13 @@ babel==2.9.1
# sphinx-intl
build==0.10.0
# via pip-tools
-certifi==2022.12.7
+cattrs==23.1.2
+ # via requests-cache
+certifi==2023.7.22
# via
# elastic-transport
# requests
- # urllib3
-cffi==1.15.0
- # via cryptography
+ # selenium
charset-normalizer==3.1.0
# via requests
click==8.1.3
@@ -40,10 +42,6 @@ click==8.1.3
# sphinx-intl
colorama==0.4.4
# via sphinx-autobuild
-cryptography==41.0.1
- # via
- # pyopenssl
- # urllib3
docutils==0.18
# via
# myst-parser
@@ -53,14 +51,15 @@ elastic-transport==8.4.0
elasticsearch[requests]==8.6.2
# via ocdsindex
exceptiongroup==1.0.0
- # via pytest
+ # via
+ # cattrs
+ # pytest
h11==0.13.0
# via wsproto
idna==2.10
# via
# requests
# trio
- # urllib3
imagesize==1.4.1
# via sphinx
iniconfig==1.1.1
@@ -93,7 +92,7 @@ myst-parser==0.18.1
# via -r common-requirements.in
ocds-babel==0.3.1
# via -r common-requirements.in
-ocdsextensionregistry==0.1.12
+ocdsextensionregistry==0.3.8
# via -r common-requirements.in
ocdsindex==0.2.0
# via -r common-requirements.in
@@ -106,14 +105,12 @@ packaging==21.3
# sphinx
pip-tools==6.13.0
# via -r common-requirements.in
+platformdirs==3.9.1
+ # via requests-cache
pluggy==0.13.1
# via pytest
-pycparser==2.21
- # via cffi
-pygments==2.13.0
+pygments==2.15.1
# via sphinx
-pyopenssl==22.0.0
- # via urllib3
pyparsing==2.4.7
# via packaging
pyproject-hooks==1.0.0
@@ -124,7 +121,7 @@ pytest==7.2.0
# via -r common-requirements.in
pytz==2021.1
# via babel
-pyyaml==5.4.1
+pyyaml==6.0.1
# via myst-parser
requests==2.31.0
# via
@@ -132,12 +129,14 @@ requests==2.31.0
# ocdsextensionregistry
# requests-cache
# sphinx
-requests-cache==0.5.2
+requests-cache==1.1.0
# via ocdsextensionregistry
-selenium==4.1.3
+selenium==4.11.2
# via -r common-requirements.in
six==1.15.0
- # via livereload
+ # via
+ # livereload
+ # url-normalize
sniffio==1.2.0
# via trio
snowballstemmer==2.1.0
@@ -171,7 +170,7 @@ tomli==2.0.1
# build
# pyproject-hooks
# pytest
-tornado==6.3.2
+tornado==6.3.3
# via livereload
trio==0.20.0
# via
@@ -180,13 +179,18 @@ trio==0.20.0
trio-websocket==0.9.2
# via selenium
typing-extensions==4.4.0
- # via myst-parser
+ # via
+ # cattrs
+ # myst-parser
uc-micro-py==1.0.1
# via linkify-it-py
-urllib3[secure,socks]==1.26.5
+url-normalize==1.4.3
+ # via requests-cache
+urllib3[socks]==1.26.5
# via
# elastic-transport
# requests
+ # requests-cache
# selenium
wheel==0.38.4
# via pip-tools
diff --git a/include/config.mk b/include/config.mk
index ca85dfda9..8be5c3709 100644
--- a/include/config.mk
+++ b/include/config.mk
@@ -12,13 +12,15 @@ LOCALE_DIR=docs/locale
# Directory in which to build documentation files.
BUILD_DIR=build
# Extra build files or directories. (These should match paths in .gitignore.)
-EXTRA_BUILD_FILES=chromedriver*
+EXTRA_BUILD_FILES=
# Files that are built and distributed (you may use Bash extended globbing).
DIST_FILES=
# Directory in which to build .pot files.
POT_DIR=$(BUILD_DIR)/locale
# The prefix, if any, to the schema and codelists domains.
DOMAIN_PREFIX=
+# The Transifex organization name.
+TRANSIFEX_ORGANIZATION=open-contracting-partnership-1
# The Transifex project name.
TRANSIFEX_PROJECT=open-contracting-standard-1-1
# Any additional extract targets.
From 159ae05e951d07afe8af538ab01ced16b2b0dff1 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Tue, 22 Aug 2023 12:03:20 -0400
Subject: [PATCH 02/47] test: Ignore "ResourceWarning: unclosed file
<_io.BufferedWriter name='/dev/null'>" (unknown source)
(cherry picked from commit bdcaaefd882e220f83b56a4da1e8a118392fbbe0)
---
.github/workflows/ci.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ac16da1c8..6f2618a9e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,7 +23,8 @@ jobs:
# - run: |
# make linkcheck
# rm -f output.json output.txt
- - run: pytest -W error
+ # "ResourceWarning: unclosed file <_io.BufferedWriter name='/dev/null'>"
+ - run: pytest -W error -W ignore::ResourceWarning
# Deploy the built documentation to the staging directory.
- if: github.event_name == 'push'
uses: shimataro/ssh-key-action@v2
From eb365706ea070b2fe7f922489d4f4d0bfe87c770 Mon Sep 17 00:00:00 2001
From: Duncan Dewhurst
Date: Thu, 12 Oct 2023 10:59:54 +1300
Subject: [PATCH 03/47] .gitignore: Add python-version (used by pyenv)
(cherry picked from commit cbc010dfde266a38343137087602205cbd5744d9)
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index 2a40f9487..f1d29bd7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@
/docs/_static/patched
/src
*.pyc
+.python-version
From 8780606368675a95eca48865d5ee2eb1208f391d Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Fri, 3 Nov 2023 02:31:12 -0400
Subject: [PATCH 04/47] ci: Run lint on both push and pull_request events (only
pull_request events set GITHUB_BASE_REF, needed by test_json.py)
(cherry picked from commit caf95de7a7527dba4cb628e36561874e32b4e946)
---
.github/workflows/lint.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 9a312dfbf..b74d3731a 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -4,7 +4,6 @@ env:
BASEDIR: https://raw.githubusercontent.com/open-contracting/standard-maintenance-scripts/main
jobs:
build:
- if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
From 19173f091c53e9fedc67387f936185ff98cb5dfa Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Fri, 3 Nov 2023 03:57:03 -0400
Subject: [PATCH 05/47] ci: Run lint on only pushes to protected branches or
tags
(cherry picked from commit df4e7d9792a688aa1948ac743d4400dd974f2b45)
---
.github/workflows/lint.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index b74d3731a..73b7bf56e 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -4,6 +4,7 @@ env:
BASEDIR: https://raw.githubusercontent.com/open-contracting/standard-maintenance-scripts/main
jobs:
build:
+ if: github.event_name == 'pull_request' || startsWith(github.event.ref, 'refs/tags/') || startsWith(github.event.ref, 'refs/heads/1.1') || startsWith(github.event.ref, 'refs/heads/1.2')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
From df72e0e3a08ae6fbc6392e16da59e674171eeba3 Mon Sep 17 00:00:00 2001
From: Duncan Dewhurst
Date: Tue, 14 Nov 2023 10:11:35 +1300
Subject: [PATCH 06/47] requirements.txt: Upgrade
sphinxcontrib-opendataservices-jsonschema
(cherry picked from commit 34e7631bb579658470badeca2de7ebe6e8854257)
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 3399e19cf..9c2ea5690 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,5 +5,5 @@
ocdskit==1.1.3
sphinx-design==0.4.1
sphinxcontrib-opencontracting==0.0.8
-sphinxcontrib-opendataservices-jsonschema==0.5.1
+sphinxcontrib-opendataservices-jsonschema==0.6.1
sphinxcontrib-opendataservices==0.5.0
From da11e1501de05af0d2c154ba2a24afa08c9546a7 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Mon, 29 Jan 2024 16:23:55 -0500
Subject: [PATCH 07/47] build: Update to latest profile template
(cherry picked from commit 8dccac8d93eecfccdd1479617b30c7091abea8fe)
---
.github/dependabot.yml | 6 ++++++
.github/workflows/ci.yml | 4 ++--
.github/workflows/js.yml | 4 ++--
.github/workflows/lint.yml | 6 +++---
.github/workflows/shell.yml | 2 +-
.github/workflows/spellcheck.yml | 4 ++--
.gitignore | 1 -
common-requirements.txt | 7 ++++---
include/common.mk | 10 +++++-----
include/prologue.mk | 2 +-
script/update | 2 +-
11 files changed, 27 insertions(+), 21 deletions(-)
create mode 100644 .github/dependabot.yml
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..123014908
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6f2618a9e..384eee48a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,10 +5,10 @@ jobs:
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
submodules: true
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: pip
diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml
index 47d8e09f7..f60649c7a 100644
--- a/.github/workflows/js.yml
+++ b/.github/workflows/js.yml
@@ -5,8 +5,8 @@ jobs:
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-node@v3
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: 18
- run: npm install standard
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 73b7bf56e..68a36675d 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -4,11 +4,11 @@ env:
BASEDIR: https://raw.githubusercontent.com/open-contracting/standard-maintenance-scripts/main
jobs:
build:
- if: github.event_name == 'pull_request' || startsWith(github.event.ref, 'refs/tags/') || startsWith(github.event.ref, 'refs/heads/1.1') || startsWith(github.event.ref, 'refs/heads/1.2')
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: pip
diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml
index 182176554..ad37d1558 100644
--- a/.github/workflows/shell.yml
+++ b/.github/workflows/shell.yml
@@ -5,7 +5,7 @@ jobs:
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Install shellcheck
run: |
sudo apt update
diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml
index 5d16e5fe0..300eefc34 100644
--- a/.github/workflows/spellcheck.yml
+++ b/.github/workflows/spellcheck.yml
@@ -5,8 +5,8 @@ jobs:
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-python@v4
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.10'
- run: pip install codespell
diff --git a/.gitignore b/.gitignore
index f1d29bd7b..2a40f9487 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,3 @@
/docs/_static/patched
/src
*.pyc
-.python-version
diff --git a/common-requirements.txt b/common-requirements.txt
index 7780d73bc..1fab18073 100644
--- a/common-requirements.txt
+++ b/common-requirements.txt
@@ -64,7 +64,7 @@ imagesize==1.4.1
# via sphinx
iniconfig==1.1.1
# via pytest
-jinja2==3.1.2
+jinja2==3.1.3
# via
# myst-parser
# sphinx
@@ -103,7 +103,7 @@ packaging==21.3
# build
# pytest
# sphinx
-pip-tools==6.13.0
+pip-tools==7.3.0
# via -r common-requirements.in
platformdirs==3.9.1
# via requests-cache
@@ -168,6 +168,7 @@ sphinxcontrib-serializinghtml==1.1.5
tomli==2.0.1
# via
# build
+ # pip-tools
# pyproject-hooks
# pytest
tornado==6.3.3
@@ -186,7 +187,7 @@ uc-micro-py==1.0.1
# via linkify-it-py
url-normalize==1.4.3
# via requests-cache
-urllib3[socks]==1.26.5
+urllib3[socks]==1.26.18
# via
# elastic-transport
# requests
diff --git a/include/common.mk b/include/common.mk
index aafd8e0ee..424e82269 100644
--- a/include/common.mk
+++ b/include/common.mk
@@ -1,15 +1,15 @@
# See https://github.com/datamade/data-making-guidelines
-# See http://clarkgrubb.com/makefile-style-guide#phony-target-arg
+# See https://clarkgrubb.com/makefile-style-guide#phony-target-arg
FORCE:
-# http://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html
+# https://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html
COMMA := ,
SPACE :=
SPACE +=
COMMA_SEPARATED_TRANSLATIONS=$(subst $(SPACE),$(COMMA),$(TRANSLATIONS:.%=%))
-# See http://clarkgrubb.com/makefile-style-guide#phony-targets
+# See https://clarkgrubb.com/makefile-style-guide#phony-targets
.PHONY: clean
clean:
rm -rf $(BUILD_DIR)
@@ -45,7 +45,7 @@ extract_schema: $(POT_DIR)
# The codelist CSV files and JSON Schema files must be present for the `csv-table-no-translate` and `jsonschema`
# directives to succeed, but the contents of the files have no effect on the generated .pot files.
-# See http://www.sphinx-doc.org/en/stable/builders.html#sphinx.builders.gettext.MessageCatalogBuilder
+# See https://www.sphinx-doc.org/en/master/usage/builders/index.html#sphinx.builders.gettext.MessageCatalogBuilder
.PHONY: extract_markdown
extract_markdown: current_lang.en
sphinx-build -nW --keep-going -q -b gettext $(DOCS_DIR) $(POT_DIR)
@@ -110,7 +110,7 @@ clean_current_lang:
### Build
# Build the source documentation.
-# See http://www.sphinx-doc.org/en/stable/builders.html#sphinx.builders.html.DirectoryHTMLBuilder
+# See https://www.sphinx-doc.org/en/master/usage/builders/index.html#sphinx.builders.html.DirectoryHTMLBuilder
.PHONY: build_source
build_source: current_lang.en
sphinx-build -nW --keep-going -q -b dirhtml $(DOCS_DIR) $(BUILD_DIR)/en
diff --git a/include/prologue.mk b/include/prologue.mk
index b512b7b3e..166f3ab5d 100644
--- a/include/prologue.mk
+++ b/include/prologue.mk
@@ -1,4 +1,4 @@
-# See http://clarkgrubb.com/makefile-style-guide#prologue
+# See https://clarkgrubb.com/makefile-style-guide#prologue
MAKEFLAGS += --warn-undefined-variables
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
diff --git a/script/update b/script/update
index 5c9d03fc6..ada17caa4 100755
--- a/script/update
+++ b/script/update
@@ -4,7 +4,7 @@ set -euo pipefail
function main {
mkdir -p script include tests
- for f in Makefile common-requirements.in common-requirements.txt .github/workflows/lint.yml .github/workflows/shell.yml docs/_static/favicon-16x16.ico include/common.mk include/prologue.mk include/header.html script/diff script/update tests/conftest.py tests/test_common.py; do
+ for f in Makefile common-requirements.in common-requirements.txt .github/dependabot.yml .github/workflows/lint.yml .github/workflows/shell.yml docs/_static/favicon-16x16.ico include/common.mk include/prologue.mk include/header.html script/diff script/update tests/conftest.py tests/test_common.py; do
curl -sS -o $f https://raw.githubusercontent.com/open-contracting/standard_profile_template/latest/$f
done
From 465dd353f2a86f5591dea1f1c256682a40f19145 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 28 Feb 2024 00:14:33 +0000
Subject: [PATCH 08/47] build(deps): bump bcomnes/netrc-creds from 2 to 3
Bumps [bcomnes/netrc-creds](https://github.com/bcomnes/netrc-creds) from 2 to 3.
- [Release notes](https://github.com/bcomnes/netrc-creds/releases)
- [Changelog](https://github.com/bcomnes/netrc-creds/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bcomnes/netrc-creds/compare/v2...v3)
---
updated-dependencies:
- dependency-name: bcomnes/netrc-creds
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
(cherry picked from commit 55909da4c27348166e816d8bf6dd18e033a9ba5d)
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 384eee48a..966087019 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,7 +32,7 @@ jobs:
key: ${{ secrets.PRIVATE_KEY }}
known_hosts: standard.open-contracting.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGveFGTJ9yyObNGDUCUyzyFkm6Kzh3YqIt1qB7B/KU6E
- if: github.event_name == 'push'
- uses: bcomnes/netrc-creds@v2
+ uses: bcomnes/netrc-creds@v3
with:
machine: standard.open-contracting.org
login: manage
From 3fcc1cc05b0502d65ecbca6f0d853f7c0d7e81e3 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Thu, 4 Apr 2024 14:00:18 -0400
Subject: [PATCH 09/47] build: Update to latest profile template
(cherry picked from commit 1bf6a0c3d6367e9fb8604f3d532946e4194962ed)
---
.github/workflows/shell.yml | 6 ++----
common-requirements.txt | 2 +-
docs/conf.py | 1 +
docs/guidance/index.md | 2 +-
docs/locale/es/LC_MESSAGES/guidance/index.po | 4 ++--
docs/locale/fr/LC_MESSAGES/guidance/index.po | 2 +-
6 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml
index ad37d1558..e87989085 100644
--- a/.github/workflows/shell.yml
+++ b/.github/workflows/shell.yml
@@ -6,10 +6,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - name: Install shellcheck
- run: |
+ - run: |
sudo apt update
- sudo apt install shellcheck
- - run: sudo snap install shfmt
+ sudo apt install shellcheck shfmt
- run: shellcheck $(shfmt -f .)
- run: shfmt -d -i 4 -sr $(shfmt -f .)
diff --git a/common-requirements.txt b/common-requirements.txt
index 1fab18073..089b9bfb1 100644
--- a/common-requirements.txt
+++ b/common-requirements.txt
@@ -133,7 +133,7 @@ requests-cache==1.1.0
# via ocdsextensionregistry
selenium==4.11.2
# via -r common-requirements.in
-six==1.15.0
+six==1.16.0
# via
# livereload
# url-normalize
diff --git a/docs/conf.py b/docs/conf.py
index 1d3cc89bc..7ae7ca563 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -111,6 +111,7 @@
'license_url': f'{repository_url}/blob/HEAD/LICENSE',
'repository_url': repository_url,
}
+html_short_title = f'{html_theme_options["short_project"]} v{release}'
# List the extension identifiers and versions that should be part of this specification. The extensions must be in
# the extension registry: https://github.com/open-contracting/extension_registry/blob/main/extension_versions.csv
diff --git a/docs/guidance/index.md b/docs/guidance/index.md
index 10bfe575a..d2692297b 100644
--- a/docs/guidance/index.md
+++ b/docs/guidance/index.md
@@ -6,7 +6,7 @@ The four phases of implementation described in this guide have helped implemente
Read the guidance to understand the main steps to implement OCDS and to find supporting resources. Detailed guidance on specific topics and tasks is provided in sub-pages.
-Use the [OCDS Implementation Checklist](https://www.open-contracting.org/resources/ocds-implementation-checklist) to keep track of your progress.
+Use the [OCDS Implementation Checklist](https://www.open-contracting.org/resources/ocds-implementation-checklist/) to keep track of your progress.
```{toctree}
:maxdepth: 2
diff --git a/docs/locale/es/LC_MESSAGES/guidance/index.po b/docs/locale/es/LC_MESSAGES/guidance/index.po
index 457625b13..06d357115 100644
--- a/docs/locale/es/LC_MESSAGES/guidance/index.po
+++ b/docs/locale/es/LC_MESSAGES/guidance/index.po
@@ -55,11 +55,11 @@ msgstr ""
#: ../../docs/guidance/index.md:9
msgid ""
"Use the [OCDS Implementation Checklist](https://www.open-"
-"contracting.org/resources/ocds-implementation-checklist) to keep track of "
+"contracting.org/resources/ocds-implementation-checklist/) to keep track of "
"your progress."
msgstr ""
"Utilice la [Lista de Verificación de Implementación](https://www.open-"
-"contracting.org/resources/ocds-implementation-checklist) para hacer un "
+"contracting.org/resources/ocds-implementation-checklist/) para hacer un "
"seguimiento de su progreso."
#: ../../docs/guidance/index.md:20
diff --git a/docs/locale/fr/LC_MESSAGES/guidance/index.po b/docs/locale/fr/LC_MESSAGES/guidance/index.po
index 736c5359a..21f566677 100644
--- a/docs/locale/fr/LC_MESSAGES/guidance/index.po
+++ b/docs/locale/fr/LC_MESSAGES/guidance/index.po
@@ -46,7 +46,7 @@ msgstr ""
#: ../../docs/guidance/index.md:9
msgid ""
"Use the [OCDS Implementation Checklist](https://www.open-"
-"contracting.org/resources/ocds-implementation-checklist) to keep track of "
+"contracting.org/resources/ocds-implementation-checklist/) to keep track of "
"your progress."
msgstr ""
From 7340679a320f7755cb840d1dfc7936639da7d315 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Wed, 12 Jun 2024 18:15:10 -0400
Subject: [PATCH 10/47] chore: Update JS style to latest profile template
(cherry picked from commit f049f111d13611bbf6cc32b269cb5202755b1c70)
---
.github/workflows/js.yml | 7 +-
docs/_static/script.js | 138 ++++++++++++++++++++-------------------
2 files changed, 74 insertions(+), 71 deletions(-)
diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml
index f60649c7a..4c8147e26 100644
--- a/.github/workflows/js.yml
+++ b/.github/workflows/js.yml
@@ -6,8 +6,5 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-node@v4
- with:
- node-version: 18
- - run: npm install standard
- - run: npx standard docs/_static/script.js
+ - uses: biomejs/setup-biome@v2
+ - run: biome ci --indent-style=space --line-width=119 docs/_static/script.js
diff --git a/docs/_static/script.js b/docs/_static/script.js
index e46cd638a..ffd653486 100644
--- a/docs/_static/script.js
+++ b/docs/_static/script.js
@@ -1,35 +1,40 @@
/* global XMLHttpRequest, location, renderjson */
-const pathComponents = window.location.pathname.split('/')
-if (pathComponents[pathComponents.length - 2] === 'codelists') {
- document.querySelector('.wy-nav-content').style.maxWidth = 'none'
+const pathComponents = window.location.pathname.split("/");
+if (pathComponents[pathComponents.length - 2] === "codelists") {
+ document.querySelector(".wy-nav-content").style.maxWidth = "none";
}
// FYI: extensionlist appears many times on /schema/reference/
-if (document.querySelector('.extension_list')) {
- const language = location.pathname.split('/')[2]
+if (document.querySelector(".extension_list")) {
+ const language = location.pathname.split("/")[2];
// Append an empty list for community extensions.
- document.querySelectorAll('.extension_list .hide').forEach(element => {
- const dl = document.createElement('dl')
- dl.className = 'simple community-list hide'
- element.insertAdjacentElement('afterend', dl)
- element.innerHTML = `${element.innerHTML}`
- })
+ for (const element of document.querySelectorAll(".extension_list .hide")) {
+ const dl = document.createElement("dl");
+ dl.className = "simple community-list hide";
+ element.insertAdjacentElement("afterend", dl);
+ element.innerHTML = `${element.innerHTML}`;
+ }
// Get the community extensions to add to the documentation.
- const request = new XMLHttpRequest()
- request.open('GET', 'https://raw.githubusercontent.com/open-contracting/extension_registry/main/build/extensions.json')
- request.responseType = 'json'
+ const request = new XMLHttpRequest();
+ request.open(
+ "GET",
+ "https://raw.githubusercontent.com/open-contracting/extension_registry/main/build/extensions.json",
+ );
+ request.responseType = "json";
request.onload = () => {
if (request.status >= 200 && request.status < 400) {
// Add community extensions.
- request.response.extensions.forEach(extension => {
+ for (const extension of request.response.extensions) {
if (!extension.core) {
- const div = document.getElementById(`extensionlist-${extension.category}`)
+ const div = document.getElementById(`extensionlist-${extension.category}`);
if (div) {
- div.querySelector('.community-list').insertAdjacentHTML('beforeend', `
+ div.querySelector(".community-list").insertAdjacentHTML(
+ "beforeend",
+ `
${extension.name[language] || extension.name.en}
@@ -38,79 +43,80 @@ if (document.querySelector('.extension_list')) {
${extension.description[language] || extension.description.en}
- `)
- div.querySelectorAll('.hide').forEach(element => {
- element.classList.remove('hide')
- })
+ `,
+ );
+ for (const element of div.querySelectorAll(".hide")) {
+ element.classList.remove("hide");
+ }
}
}
- })
+ }
// Remove empty extension lists.
- document.querySelectorAll('.extension_list').forEach(element => {
- if (!element.querySelector('a')) {
- element.remove()
+ for (const element of document.querySelectorAll(".extension_list")) {
+ if (!element.querySelector("a")) {
+ element.remove();
}
- })
+ }
}
- }
+ };
- request.send()
+ request.send();
}
-document.querySelectorAll('.expandjson').forEach(element => {
- const defaultOpen = []
- let fileClassName
- element.classList.forEach(className => {
- if (className.substring(0, 7) === 'expand-' && className.length > 7) {
- defaultOpen.push(className.substring(7))
+for (const element of document.querySelectorAll(".expandjson")) {
+ const defaultOpen = [];
+ let fileClassName;
+ for (const className of element.classList) {
+ if (className.substring(0, 7) === "expand-" && className.length > 7) {
+ defaultOpen.push(className.substring(7));
}
- if (className.substring(0, 5) === 'file-') {
- fileClassName = className
+ if (className.substring(0, 5) === "file-") {
+ fileClassName = className;
}
- })
+ }
- let data = JSON.parse(element.textContent)
+ let data = JSON.parse(element.textContent);
// If the jsoninclude directive indexed to a JSON array (a common mistake), only display the first entry.
if (data.length) {
- data = data[0]
+ data = data[0];
}
- const replacement = renderjson.set_show_to_level(1).set_max_string_length(100).set_default_open(defaultOpen)(data)
+ const replacement = renderjson.set_show_to_level(1).set_max_string_length(100).set_default_open(defaultOpen)(data);
// element.firstElementChild.replaceWith(replacement) // https://caniuse.com/mdn-api_parentnode_firstelementchild
- element.replaceChild(replacement, element.querySelector('.highlight-json'))
+ element.replaceChild(replacement, element.querySelector(".highlight-json"));
- const container = element.previousElementSibling
- let select
- if (container && container.classList.contains('selection-container')) {
+ const container = element.previousElementSibling;
+ let select;
+ if (container?.classList.contains("selection-container")) {
// Hide additional examples.
- element.style.display = 'none'
- container.appendChild(element)
+ element.style.display = "none";
+ container.appendChild(element);
// Display the select element if there are multiple options.
- select = container.querySelector('select')
- select.style.display = ''
+ select = container.querySelector("select");
+ select.style.display = "";
} else {
- const div = document.createElement('div')
- element.insertAdjacentElement('beforebegin', div)
- div.className = 'selection-container'
- div.appendChild(element)
+ const div = document.createElement("div");
+ element.insertAdjacentElement("beforebegin", div);
+ div.className = "selection-container";
+ div.appendChild(element);
- select = document.createElement('select')
- div.insertAdjacentElement('afterbegin', select)
+ select = document.createElement("select");
+ div.insertAdjacentElement("afterbegin", select);
// Hide the select element if there is one option.
- select.style.display = 'none'
- select.addEventListener('change', () => {
- div.querySelectorAll('.expandjson').forEach(child => {
- child.style.display = 'none'
- })
- div.querySelector(`.${select.value}`).style.display = ''
- })
+ select.style.display = "none";
+ select.addEventListener("change", () => {
+ for (const child of div.querySelectorAll(".expandjson")) {
+ child.style.display = "none";
+ }
+ div.querySelector(`.${select.value}`).style.display = "";
+ });
}
- const option = document.createElement('option')
- option.value = fileClassName
- option.textContent = fileClassName.replace('file-', '')
- select.appendChild(option)
-})
+ const option = document.createElement("option");
+ option.value = fileClassName;
+ option.textContent = fileClassName.replace("file-", "");
+ select.appendChild(option);
+}
From 930b38adb5fb4ba1b2a9d0025a16a95802f00e3b Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Wed, 12 Jun 2024 18:15:51 -0400
Subject: [PATCH 11/47] chore: Update to latest profile template
(cherry picked from commit 796405a8fa335ba37f018e9e9703962b15b19be7)
---
.github/workflows/shell.yml | 3 ++-
common-requirements.txt | 10 +++++-----
script/diff | 7 ++++---
script/update | 7 ++++---
4 files changed, 15 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml
index e87989085..fe96765b8 100644
--- a/.github/workflows/shell.yml
+++ b/.github/workflows/shell.yml
@@ -8,6 +8,7 @@ jobs:
- uses: actions/checkout@v4
- run: |
sudo apt update
- sudo apt install shellcheck shfmt
+ sudo apt install devscripts shellcheck shfmt
+ - run: checkbashisms $(shfmt -f .)
- run: shellcheck $(shfmt -f .)
- run: shfmt -d -i 4 -sr $(shfmt -f .)
diff --git a/common-requirements.txt b/common-requirements.txt
index 089b9bfb1..ff4de52f3 100644
--- a/common-requirements.txt
+++ b/common-requirements.txt
@@ -56,7 +56,7 @@ exceptiongroup==1.0.0
# pytest
h11==0.13.0
# via wsproto
-idna==2.10
+idna==3.7
# via
# requests
# trio
@@ -64,7 +64,7 @@ imagesize==1.4.1
# via sphinx
iniconfig==1.1.1
# via pytest
-jinja2==3.1.3
+jinja2==3.1.4
# via
# myst-parser
# sphinx
@@ -123,7 +123,7 @@ pytz==2021.1
# via babel
pyyaml==6.0.1
# via myst-parser
-requests==2.31.0
+requests==2.32.2
# via
# elasticsearch
# ocdsextensionregistry
@@ -151,7 +151,7 @@ sphinx==5.3.0
# sphinx-intl
sphinx-autobuild==2021.3.14
# via -r common-requirements.in
-sphinx-intl==2.1.0
+sphinx-intl==2.2.0
# via -r common-requirements.in
sphinxcontrib-applehelp==1.0.2
# via sphinx
@@ -171,7 +171,7 @@ tomli==2.0.1
# pip-tools
# pyproject-hooks
# pytest
-tornado==6.3.3
+tornado==6.4.1
# via livereload
trio==0.20.0
# via
diff --git a/script/diff b/script/diff
index 988c0fff9..cf7e85060 100755
--- a/script/diff
+++ b/script/diff
@@ -1,6 +1,7 @@
-#!/usr/bin/env bash
-# No -e, because diff fails.
-set -uo pipefail
+#!/bin/sh
+
+# No -e, because diff uses exit status 1 when differences are found.
+set -u
curl -sS https://raw.githubusercontent.com/open-contracting/standard_profile_template/latest/docs/conf.py | diff -u - docs/conf.py
diff --git a/script/update b/script/update
index ada17caa4..cba8c6fc2 100755
--- a/script/update
+++ b/script/update
@@ -1,7 +1,8 @@
-#!/usr/bin/env bash
-set -euo pipefail
+#!/bin/sh
-function main {
+set -eu
+
+main() {
mkdir -p script include tests
for f in Makefile common-requirements.in common-requirements.txt .github/dependabot.yml .github/workflows/lint.yml .github/workflows/shell.yml docs/_static/favicon-16x16.ico include/common.mk include/prologue.mk include/header.html script/diff script/update tests/conftest.py tests/test_common.py; do
From 5491ce41725e2acf66762e917d5e29566c7cd7cb Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Wed, 12 Jun 2024 18:22:45 -0400
Subject: [PATCH 12/47] ci: Add sme exception
(cherry picked from commit b95e9ccbc5ec552ff182f9e7349a9a9fd94a9b1a)
---
.github/workflows/spellcheck.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml
index 300eefc34..b60b28a2b 100644
--- a/.github/workflows/spellcheck.yml
+++ b/.github/workflows/spellcheck.yml
@@ -10,4 +10,4 @@ jobs:
with:
python-version: '3.10'
- run: pip install codespell
- - run: codespell -S .git,docson,locale,examples,country.csv,currency.csv,language.csv,mediaType.csv -L fo,zar .
+ - run: codespell -S .git,docson,locale,examples,country.csv,currency.csv,language.csv,mediaType.csv -L fo,sme,zar,SME .
From 2055d3facf061a99388dfcfb0bdf8e87cc618b9f Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Wed, 23 Oct 2024 17:30:36 -0400
Subject: [PATCH 13/47] chore: ruff check
(cherry picked from commit 987f40e58399341b34f77eba5b45f4e20c0bffb3)
---
docs/conf.py | 5 +-
manage.py | 152 +++++++++++++--------------------
pyproject.toml | 35 ++++++++
tests/__init__.py | 4 +-
tests/test_common.py | 4 +-
tests/test_schema_integrity.py | 2 +-
6 files changed, 100 insertions(+), 102 deletions(-)
create mode 100644 pyproject.toml
diff --git a/docs/conf.py b/docs/conf.py
index 7ae7ca563..448261356 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/manage.py b/manage.py
index 0b5a40d45..7da07066a 100755
--- a/manage.py
+++ b/manage.py
@@ -29,7 +29,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):
@@ -38,7 +38,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": {
@@ -61,7 +61,7 @@ def custom_warning_formatter(message, category, filename, lineno, line=None):
}
}
}
-''')
+""")
common_versioned_definitions = {
'StringNullUriVersioned': {
@@ -116,35 +116,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)
@@ -155,18 +146,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]
@@ -174,16 +161,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():
@@ -191,15 +176,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)
@@ -211,7 +195,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
@@ -280,9 +264,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'
@@ -291,9 +273,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}')
@@ -322,9 +302,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)
@@ -340,15 +318,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)
@@ -356,12 +332,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
+ 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.
@@ -447,10 +421,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:
@@ -491,7 +462,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
@@ -553,7 +524,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')
@@ -564,16 +536,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'):
match = node.xpath('./Ccy')
# Entries like Antarctica have no universal currency.
@@ -584,23 +554,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:
@@ -610,23 +580,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
@@ -660,8 +627,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:
@@ -673,7 +639,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])
@@ -683,9 +649,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)
@@ -737,9 +701,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:
@@ -749,10 +711,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.')
@@ -772,11 +734,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
'
+ template = (
+ ''
+ )
document.xpath('//h1')[0].addnext(lxml.etree.XML(template % {
'note': _('Note'), 'message': message % {'url': pattern.format('en')}}))
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..8cd10b2a3
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,35 @@
+[project]
+name = "profile"
+version = "0.0.0"
+
+[tool.ruff]
+line-length = 119
+target-version = "py310"
+
+[tool.ruff.lint]
+select = ["ALL"]
+ignore = [
+ "ANN", "C901", "COM812", "D203", "D212", "D415", "EM", "ISC001", "PERF203", "PLR091", "Q000",
+ "D1", "D205",
+ "PTH",
+ "D200", # https://github.com/astral-sh/ruff/issues/6269
+]
+allowed-confusables = ["’"]
+
+[tool.ruff.lint.flake8-builtins]
+builtins-ignorelist = ["copyright"]
+
+[tool.ruff.lint.flake8-unused-arguments]
+ignore-variadic-names = true
+
+[tool.ruff.lint.per-file-ignores]
+"docs/conf.py" = ["D100", "INP001"]
+"tests/*" = [
+ "ARG001", "D", "FBT003", "INP001", "PLR2004", "S", "TRY003",
+]
+"manage.py" = [
+ "ARG001", # click
+ "B028", # warnings
+ "D301", # click escapes
+ "TRY003", # errors
+]
diff --git a/tests/__init__.py b/tests/__init__.py
index 07f837aa2..fc301d0a9 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -13,6 +13,6 @@
test_search_params = [
('en', r'found \d+ page\(s\) matching'),
# See https://github.com/sphinx-doc/sphinx/issues/11008
- # ('es', r'encontraron \d+ páginas que coinciden'),
- # ('fr', r'\d+ page\(s\) correspondant'),
+ # ('es', r'encontraron \d+ páginas que coinciden'), # noqa: ERA001
+ # ('fr', r'\d+ page\(s\) correspondant'), # noqa: ERA001
]
diff --git a/tests/test_common.py b/tests/test_common.py
index 803bb96a9..40e77f5a9 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -8,13 +8,13 @@
from tests import languages, test_basic_params, test_search_params
-@pytest.mark.parametrize('lang,text', test_basic_params.items())
+@pytest.mark.parametrize(('lang', 'text'), test_basic_params.items())
def test_basic(browser, server, lang, text):
browser.get(f'{server}{lang}')
assert text in browser.find_element(By.TAG_NAME, 'body').text
-@pytest.mark.parametrize('lang,regex', test_search_params)
+@pytest.mark.parametrize(('lang', 'regex'), test_search_params)
def test_search(browser, server, lang, regex):
browser.get(f'{server}{lang}')
search_box = browser.find_element(By.ID, 'rtd-search-form').find_element(By.TAG_NAME, 'input')
diff --git a/tests/test_schema_integrity.py b/tests/test_schema_integrity.py
index 6c75b7884..39ed3bd6e 100644
--- a/tests/test_schema_integrity.py
+++ b/tests/test_schema_integrity.py
@@ -10,7 +10,7 @@
sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
-from manage import get_metaschema, get_versioned_release_schema # noqa isort:skip
+from manage import get_metaschema, get_versioned_release_schema
def test_versioned_release_schema_is_in_sync():
From bd03bb270f9b0d4c882045c23f493ef2dc111510 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Wed, 23 Oct 2024 17:31:23 -0400
Subject: [PATCH 14/47] build: Update to latest profile template
(cherry picked from commit a1c73ce3e17488fda8918ab15a19d0bf725a5368)
---
.github/workflows/lint.yml | 14 ++++++
.pre-commit-config.yaml | 18 ++++++++
.python-version | 1 +
common-requirements.in | 3 +-
common-requirements.txt | 87 ++++++++++++++++++--------------------
script/update | 2 +-
tests/conftest.py | 14 +++---
tests/test_common.py | 24 +++++------
8 files changed, 94 insertions(+), 69 deletions(-)
create mode 100644 .pre-commit-config.yaml
create mode 100644 .python-version
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/script/update b/script/update
index cba8c6fc2..bd2170e08 100755
--- a/script/update
+++ b/script/update
@@ -5,7 +5,7 @@ set -eu
main() {
mkdir -p script include tests
- for f in Makefile common-requirements.in common-requirements.txt .github/dependabot.yml .github/workflows/lint.yml .github/workflows/shell.yml docs/_static/favicon-16x16.ico include/common.mk include/prologue.mk include/header.html script/diff script/update tests/conftest.py tests/test_common.py; do
+ for f in .pre-commit-config.yaml .python-version Makefile common-requirements.in common-requirements.txt pyproject.toml .github/dependabot.yml .github/workflows/lint.yml .github/workflows/shell.yml docs/_static/favicon-16x16.ico include/common.mk include/prologue.mk include/header.html script/diff script/update tests/conftest.py tests/test_common.py; do
curl -sS -o $f https://raw.githubusercontent.com/open-contracting/standard_profile_template/latest/$f
done
diff --git a/tests/conftest.py b/tests/conftest.py
index 1a96b5cdc..f5a6035f0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -7,11 +7,11 @@
from selenium.webdriver.chrome.options import Options
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
def browser(request):
options = Options()
- options.add_argument('--headless')
- options.add_argument('--no-sandbox')
+ options.add_argument("--headless")
+ options.add_argument("--no-sandbox")
browser = webdriver.Chrome(options=options)
browser.implicitly_wait(3)
@@ -21,17 +21,17 @@ def browser(request):
browser.quit()
-@pytest.fixture(scope='module')
+@pytest.fixture(scope="module")
def server(request):
- host = 'localhost'
+ host = "localhost"
port_number = 8331
- server = HTTPServer((host, port_number), partial(SimpleHTTPRequestHandler, directory='build'))
+ server = HTTPServer((host, port_number), partial(SimpleHTTPRequestHandler, directory="build"))
thread = threading.Thread(target=server.serve_forever)
thread.start()
- yield f'http://{host}:{port_number}/'
+ yield f"http://{host}:{port_number}/"
server.shutdown()
thread.join()
diff --git a/tests/test_common.py b/tests/test_common.py
index 40e77f5a9..417759f17 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -8,31 +8,31 @@
from tests import languages, test_basic_params, test_search_params
-@pytest.mark.parametrize(('lang', 'text'), test_basic_params.items())
+@pytest.mark.parametrize(("lang", "text"), test_basic_params.items())
def test_basic(browser, server, lang, text):
- browser.get(f'{server}{lang}')
- assert text in browser.find_element(By.TAG_NAME, 'body').text
+ browser.get(f"{server}{lang}")
+ assert text in browser.find_element(By.TAG_NAME, "body").text
-@pytest.mark.parametrize(('lang', 'regex'), test_search_params)
+@pytest.mark.parametrize(("lang", "regex"), test_search_params)
def test_search(browser, server, lang, regex):
- browser.get(f'{server}{lang}')
- search_box = browser.find_element(By.ID, 'rtd-search-form').find_element(By.TAG_NAME, 'input')
- search_box.send_keys('tender\n')
+ browser.get(f"{server}{lang}")
+ search_box = browser.find_element(By.ID, "rtd-search-form").find_element(By.TAG_NAME, "input")
+ search_box.send_keys("tender\n")
time.sleep(3)
- assert re.search(regex, browser.find_element(By.TAG_NAME, 'body').text)
+ assert re.search(regex, browser.find_element(By.TAG_NAME, "body").text)
# This seems to be an issue in Selenium and/or ChromeDriver.
@pytest.mark.filterwarnings("ignore:unclosed
Date: Tue, 26 Nov 2024 23:13:10 -0500
Subject: [PATCH 15/47] chore: Use double-quotes to reduce diff with profile
template
(cherry picked from commit 61bedba61f7c019ed7d8714df20bd48d3ec79a0a)
---
docs/conf.py | 120 +++++++++++++++++++++++++--------------------------
1 file changed, 58 insertions(+), 62 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index 448261356..bacca8720 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -6,10 +6,6 @@
# -- Path setup --------------------------------------------------------------
-# 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 csv
import json
import os
@@ -24,12 +20,12 @@
# -- Project information -----------------------------------------------------
-project = 'Open Contracting Data Standard'
-copyright = 'Open Contracting Partnership'
-author = 'Open Contracting Partnership'
+project = "Open Contracting Data Standard"
+copyright = "Open Contracting Partnership"
+author = "Open Contracting Partnership"
-version = '1.1'
-release = '1.1.5'
+version = "1.1"
+release = "1.1.5"
# -- General configuration ---------------------------------------------------
@@ -38,21 +34,21 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
- 'myst_parser',
- 'sphinx.ext.ifconfig',
- 'sphinxcontrib.jsonschema',
- 'sphinxcontrib.opencontracting',
- 'sphinxcontrib.opendataservices',
- 'sphinx_design',
+ "myst_parser",
+ "sphinx.ext.ifconfig",
+ "sphinxcontrib.jsonschema",
+ "sphinxcontrib.opencontracting",
+ "sphinxcontrib.opendataservices",
+ "sphinx_design",
]
# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '**/docson/[!p]**', '**/docson/package*.json']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**/docson/[!p]**", "**/docson/package*.json"]
# -- Options for HTML output -------------------------------------------------
@@ -60,84 +56,84 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_theme = 'standard_theme' # 'pydata_sphinx_theme'
+html_theme = "standard_theme" # 'pydata_sphinx_theme'
html_theme_path = [standard_theme.get_html_theme_path()]
-html_favicon = '_static/favicon-16x16.ico'
+html_favicon = "_static/favicon-16x16.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
# -- Local configuration -----------------------------------------------------
-_ = get_translation('theme')
+_ = get_translation("theme")
-profile_identifier = ''
-repository_url = 'https://github.com/open-contracting/standard'
+profile_identifier = ""
+repository_url = "https://github.com/open-contracting/standard"
# Internationalization.
gettext_compact = False
# `DOMAIN_PREFIX` from `config.mk`.
-gettext_domain_prefix = f'{profile_identifier}-' if profile_identifier else ''
-locale_dirs = ['locale/', os.path.join(standard_theme.get_html_theme_path(), 'locale')]
+gettext_domain_prefix = f"{profile_identifier}-" if profile_identifier else ""
+locale_dirs = ["locale/", os.path.join(standard_theme.get_html_theme_path(), "locale")]
# We use single quotes for codes, which docutils will change to double quotes.
# https://sourceforge.net/p/docutils/code/HEAD/tree/trunk/docutils/docutils/utils/smartquotes.py
smartquotes = False
# MyST configuration.
-myst_enable_extensions = ['linkify']
+myst_enable_extensions = ["linkify"]
myst_heading_anchors = 6
myst_heading_slug_func = make_id
# https://github.com/executablebooks/MyST-Parser/issues/357
-suppress_warnings = ['myst.anchor']
+suppress_warnings = ["myst.anchor"]
# Theme customization.
navigation_with_keys = False # restore the Sphinx default
html_context = {
- 'analytics_id': 'HTWZHRIZ',
+ "analytics_id": "HTWZHRIZ",
}
html_theme_options = {
- 'analytics_id': 'HTWZHRIZ',
- 'display_version': True,
- 'root_url': f'/profiles/{profile_identifier}' if profile_identifier else '',
- 'short_project': project.replace('Open Contracting Data Standard', 'OCDS'),
- 'copyright': copyright,
- 'license_name': 'Apache License 2.0',
- 'license_url': f'{repository_url}/blob/HEAD/LICENSE',
- 'repository_url': repository_url,
+ "analytics_id": "HTWZHRIZ",
+ "display_version": True,
+ "root_url": f"/profiles/{profile_identifier}" if profile_identifier else "",
+ "short_project": project.replace("Open Contracting Data Standard", "OCDS"),
+ "copyright": copyright,
+ "license_name": "Apache License 2.0",
+ "license_url": f"{repository_url}/blob/HEAD/LICENSE",
+ "repository_url": repository_url,
}
-html_short_title = f'{html_theme_options["short_project"]} v{release}'
+html_short_title = f"{html_theme_options["short_project"]} v{release}"
# List the extension identifiers and versions that should be part of this specification. The extensions must be in
# the extension registry: https://github.com/open-contracting/extension_registry/blob/main/extension_versions.csv
-default_extension_version = f'v{release}'
+default_extension_version = f"v{release}"
extension_versions = {
- 'bids': default_extension_version,
- 'enquiries': default_extension_version,
- 'location': default_extension_version,
- 'lots': default_extension_version,
- 'milestone_documents': default_extension_version,
- 'participation_fee': default_extension_version,
- 'process_title': default_extension_version,
+ "bids": default_extension_version,
+ "enquiries": default_extension_version,
+ "location": default_extension_version,
+ "lots": default_extension_version,
+ "milestone_documents": default_extension_version,
+ "participation_fee": default_extension_version,
+ "process_title": default_extension_version,
}
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-the-linkcheck-builder
# Ignore Google Sheets.
-linkcheck_anchors_ignore = [r'^gid=']
+linkcheck_anchors_ignore = [r"^gid="]
linkcheck_ignore = [
# Avoid GitHub.com rate limiting.
- r'^https://github.com/open-contracting/standard/(?:issues|pull)/\d+$',
+ r"^https://github.com/open-contracting/standard/(?:issues|pull)/\d+$",
# Ignore irreproducible false positives.
- r'^https://www.fcny.org/fcny/$',
- r'^http://www.eprocurementtoolkit.org/sites/default/files/2016-11/OCDS_Implemetation_Methodology_0.pdf#page=27$',
+ r"^https://www.fcny.org/fcny/$",
+ r"^http://www.eprocurementtoolkit.org/sites/default/files/2016-11/OCDS_Implemetation_Methodology_0.pdf#page=27$",
# Ignore unwanted links created by linkify.
r'^http://buyandsell.gc.ca$',
r'^http://(?:identifier|release).id$',
r'^http://vnd\.',
# Ignore expected redirects.
- r'^https://docs.google.com/spreadsheets/d/[^/]+/pub?gid=\d+&single=true&output=csv$',
+ r"^https://docs.google.com/spreadsheets/d/[^/]+/pub?gid=\d+&single=true&output=csv$",
]
@@ -145,32 +141,32 @@ def setup(app):
# The root of the repository.
basedir = Path(__file__).resolve().parents[1]
# `LOCALE_DIR` from `config.mk`.
- localedir = basedir / 'docs' / 'locale'
+ localedir = basedir / "docs" / "locale"
- language = app.config.overrides.get('language', 'en')
+ language = app.config.overrides.get("language", "en")
- headers = ['Title', 'Description', 'Extension']
+ headers = ["Title", "Description", "Extension"]
# The gettext domain for schema translations. Should match the domain in the `pybabel compile` command.
- schema_domain = f'{gettext_domain_prefix}schema'
+ schema_domain = f"{gettext_domain_prefix}schema"
# The gettext domain for codelist translations. Should match the domain in the `pybabel compile` command.
- codelists_domain = f'{gettext_domain_prefix}codelists'
+ codelists_domain = f"{gettext_domain_prefix}codelists"
- standard_dir = basedir / 'schema'
- standard_build_dir = basedir / 'build' / language
+ standard_dir = basedir / "schema"
+ standard_build_dir = basedir / "build" / language
- branch = os.getenv('GITHUB_REF_NAME', 'latest')
+ branch = os.getenv("GITHUB_REF_NAME", "latest")
translate([
# The glob patterns in `babel_ocds_schema.cfg` should match these filenames.
- (glob(str(standard_dir / '*-schema.json')), standard_build_dir, schema_domain),
+ (glob(str(standard_dir / "*-schema.json")), standard_build_dir, schema_domain),
# The glob patterns in `babel_ocds_codelist.cfg` should match these.
- (glob(str(standard_dir / 'codelists' / '*.csv')), standard_build_dir / 'codelists', codelists_domain),
+ (glob(str(standard_dir / "codelists" / "*.csv")), standard_build_dir / "codelists", codelists_domain),
], localedir, language, headers, version=branch)
- with (standard_build_dir / 'release-schema.json').open() as f:
+ with (standard_build_dir / "release-schema.json").open() as f:
fieldnames, rows = mapping_sheet(json.load(f), infer_required=True)
- with (standard_build_dir / 'release-schema.csv').open('w') as f:
+ with (standard_build_dir / "release-schema.csv").open("w") as f:
writer = csv.DictWriter(f, fieldnames)
writer.writeheader()
writer.writerows(rows)
From e354e82e7dbb931e343f73beb56b8cf4c6ca3aa1 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Tue, 26 Nov 2024 23:13:33 -0500
Subject: [PATCH 16/47] build: Update to latest profile template
(cherry picked from commit 0bd578120b25cd638dd0cdade00344a83b80b51b)
---
.github/workflows/ci.yml | 53 +++++++-------------------------
.github/workflows/js.yml | 11 +++----
.github/workflows/lint.yml | 36 ++++------------------
.github/workflows/shell.yml | 13 ++------
.github/workflows/spellcheck.yml | 15 +++------
docs/conf.py | 2 +-
script/diff | 2 +-
7 files changed, 30 insertions(+), 102 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 966087019..b92694c15 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,46 +2,15 @@ name: CI
on: [push, pull_request]
jobs:
build:
- if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: true
- - uses: actions/setup-python@v5
- with:
- python-version: '3.10'
- cache: pip
- cache-dependency-path: '**/requirements*.txt'
- # Don't install editable projects in the current working directory.
- # https://pip.pypa.io/en/latest/reference/pip_install/#install-src
- - run: pip install --src $GITHUB_WORKSPACE/../src -r requirements.txt
- - if: github.repository == 'open-contracting/standard_profile_template'
- run: make update extract
- - run: make
- # Disable linkcheck as it is too slow for development.
- # - run: |
- # make linkcheck
- # rm -f output.json output.txt
+ uses: open-contracting/.github/.github/workflows/ci-profile.yml@main
+ secrets:
+ private-key: ${{ secrets.PRIVATE_KEY }}
+ elasticsearch-password: ${{ secrets.ELASTICSEARCH_PASSWORD }}
+ with:
+ path-prefix: ''
+ version: '1.1'
+ production-refs: '["refs/heads/1.0", "refs/heads/1.1"]'
+ # Linkcheck is slow for development.
+ linkcheck: false
# "ResourceWarning: unclosed file <_io.BufferedWriter name='/dev/null'>"
- - run: pytest -W error -W ignore::ResourceWarning
- # Deploy the built documentation to the staging directory.
- - if: github.event_name == 'push'
- uses: shimataro/ssh-key-action@v2
- with:
- key: ${{ secrets.PRIVATE_KEY }}
- known_hosts: standard.open-contracting.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGveFGTJ9yyObNGDUCUyzyFkm6Kzh3YqIt1qB7B/KU6E
- - if: github.event_name == 'push'
- uses: bcomnes/netrc-creds@v3
- with:
- machine: standard.open-contracting.org
- login: manage
- password: ${{ secrets.ELASTICSEARCH_PASSWORD }}
- - if: github.event_name == 'push' && success() && github.repository != 'open-contracting/standard_profile_template'
- env:
- PATH_PREFIX: ""
- PRODUCTION: ${{ startsWith(github.ref, 'refs/tags') || github.ref == 'refs/heads/latest' || github.ref == 'refs/heads/1.0' || github.ref == 'refs/heads/1.1' }}
- RELEASE: ${{ startsWith(github.ref, 'refs/tags') }}
- VERSION: "1.1"
- shell: bash
- run: curl -sS https://raw.githubusercontent.com/open-contracting/deploy/main/deploy-docs.sh | bash -
+ pytest-options: -W ignore::ResourceWarning
diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml
index 4c8147e26..f0d0e38b5 100644
--- a/.github/workflows/js.yml
+++ b/.github/workflows/js.yml
@@ -1,10 +1,7 @@
name: Lint JavaScript
on: [push, pull_request]
jobs:
- build:
- if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: biomejs/setup-biome@v2
- - run: biome ci --indent-style=space --line-width=119 docs/_static/script.js
+ lint:
+ uses: open-contracting/.github/.github/workflows/js.yml@main
+ with:
+ filenames: docs/_static/script.js
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 621563ce9..2f31fb710 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,33 +1,9 @@
name: Lint
on: [push, pull_request]
-env:
- BASEDIR: https://raw.githubusercontent.com/open-contracting/standard-maintenance-scripts/main
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
- run: curl -s -S --retry 3 $BASEDIR/tests/script.sh | bash -
+ lint:
+ uses: open-contracting/.github/.github/workflows/lint.yml@main
+ permissions:
+ contents: write
+ with:
+ python-version: '3.10'
diff --git a/.github/workflows/shell.yml b/.github/workflows/shell.yml
index fe96765b8..dfdb0acfb 100644
--- a/.github/workflows/shell.yml
+++ b/.github/workflows/shell.yml
@@ -1,14 +1,5 @@
name: Lint Shell
on: [push, pull_request]
jobs:
- build:
- if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - run: |
- sudo apt update
- sudo apt install devscripts shellcheck shfmt
- - run: checkbashisms $(shfmt -f .)
- - run: shellcheck $(shfmt -f .)
- - run: shfmt -d -i 4 -sr $(shfmt -f .)
+ lint:
+ uses: open-contracting/.github/.github/workflows/shell.yml@main
diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml
index b60b28a2b..d67a6d01e 100644
--- a/.github/workflows/spellcheck.yml
+++ b/.github/workflows/spellcheck.yml
@@ -1,13 +1,8 @@
name: Spell-check
on: [push, pull_request]
jobs:
- build:
- if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
- with:
- python-version: '3.10'
- - run: pip install codespell
- - run: codespell -S .git,docson,locale,examples,country.csv,currency.csv,language.csv,mediaType.csv -L fo,sme,zar,SME .
+ lint:
+ uses: open-contracting/.github/.github/workflows/spellcheck.yml@main
+ with:
+ ignore: fo,sme,zar
+ skip: docson,locale,examples,country.csv,currency.csv,language.csv,mediaType.csv
diff --git a/docs/conf.py b/docs/conf.py
index bacca8720..76273021a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -104,7 +104,7 @@
"license_url": f"{repository_url}/blob/HEAD/LICENSE",
"repository_url": repository_url,
}
-html_short_title = f"{html_theme_options["short_project"]} v{release}"
+html_short_title = f"{html_theme_options['short_project']} v{release}"
# List the extension identifiers and versions that should be part of this specification. The extensions must be in
# the extension registry: https://github.com/open-contracting/extension_registry/blob/main/extension_versions.csv
diff --git a/script/diff b/script/diff
index cf7e85060..85fb10c99 100755
--- a/script/diff
+++ b/script/diff
@@ -5,7 +5,7 @@ set -u
curl -sS https://raw.githubusercontent.com/open-contracting/standard_profile_template/latest/docs/conf.py | diff -u - docs/conf.py
-for f in .github/workflows/ci.yml .github/workflows/js.yml .github/workflows/spellcheck.yml include/config.mk; do
+for f in .github/workflows/ci.yml .github/workflows/spellcheck.yml include/config.mk; do
curl -sS https://raw.githubusercontent.com/open-contracting/standard_profile_template/latest/$f | diff -u - $f
done
From b875146e59583c811f0f31ea3fd451bb91305df8 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Wed, 27 Nov 2024 04:17:14 +0000
Subject: [PATCH 17/47] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
(cherry picked from commit 260368738b55d7d2e870c7c3618ce512cdca0c17)
---
docs/conf.py | 18 ++++++++++++------
tests/__init__.py | 14 +++++++-------
tests/test_schema_integrity.py | 16 ++++++++--------
3 files changed, 27 insertions(+), 21 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index 76273021a..5522f2259 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -156,12 +156,18 @@ def setup(app):
branch = os.getenv("GITHUB_REF_NAME", "latest")
- translate([
- # The glob patterns in `babel_ocds_schema.cfg` should match these filenames.
- (glob(str(standard_dir / "*-schema.json")), standard_build_dir, schema_domain),
- # The glob patterns in `babel_ocds_codelist.cfg` should match these.
- (glob(str(standard_dir / "codelists" / "*.csv")), standard_build_dir / "codelists", codelists_domain),
- ], localedir, language, headers, version=branch)
+ translate(
+ [
+ # The glob patterns in `babel_ocds_schema.cfg` should match these filenames.
+ (glob(str(standard_dir / "*-schema.json")), standard_build_dir, schema_domain),
+ # The glob patterns in `babel_ocds_codelist.cfg` should match these.
+ (glob(str(standard_dir / "codelists" / "*.csv")), standard_build_dir / "codelists", codelists_domain),
+ ],
+ localedir,
+ language,
+ headers,
+ version=branch,
+ )
with (standard_build_dir / "release-schema.json").open() as f:
fieldnames, rows = mapping_sheet(json.load(f), infer_required=True)
diff --git a/tests/__init__.py b/tests/__init__.py
index fc301d0a9..29bd6731d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,17 +1,17 @@
languages = {
- 'en': 'English',
- 'es': 'Español',
- 'fr': 'Français',
+ "en": "English",
+ "es": "Español",
+ "fr": "Français",
}
test_basic_params = {
- 'en': 'Open Contracting Data Standard',
- 'es': 'Estándar de Datos para las Contrataciones Abiertas',
- 'fr': 'Standard de Données sur la Commande Publique Ouverte',
+ "en": "Open Contracting Data Standard",
+ "es": "Estándar de Datos para las Contrataciones Abiertas",
+ "fr": "Standard de Données sur la Commande Publique Ouverte",
}
test_search_params = [
- ('en', r'found \d+ page\(s\) matching'),
+ ("en", r"found \d+ page\(s\) matching"),
# See https://github.com/sphinx-doc/sphinx/issues/11008
# ('es', r'encontraron \d+ páginas que coinciden'), # noqa: ERA001
# ('fr', r'\d+ page\(s\) correspondant'), # noqa: ERA001
diff --git a/tests/test_schema_integrity.py b/tests/test_schema_integrity.py
index 39ed3bd6e..c6fdb0f86 100644
--- a/tests/test_schema_integrity.py
+++ b/tests/test_schema_integrity.py
@@ -14,27 +14,27 @@
def test_versioned_release_schema_is_in_sync():
- with open('schema/versioned-release-validation-schema.json') as f:
+ with open("schema/versioned-release-validation-schema.json") as f:
actual = json.load(f)
- with open('schema/release-schema.json') as f:
+ with open("schema/release-schema.json") as f:
expected = get_versioned_release_schema(json.load(f))
- assert actual == expected, 'Run: python manage.py pre-commit'
+ assert actual == expected, "Run: python manage.py pre-commit"
def test_dereferenced_release_schema_is_in_sync():
- with open('schema/dereferenced-release-schema.json') as f:
+ with open("schema/dereferenced-release-schema.json") as f:
actual = json.load(f)
- with open('schema/release-schema.json') as f:
+ with open("schema/release-schema.json") as f:
expected = jsonref.load(f, merge_props=True)
- assert actual == expected, 'Run: python manage.py pre-commit'
+ assert actual == expected, "Run: python manage.py pre-commit"
def test_meta_schema_is_in_sync():
- with open('schema/meta-schema.json') as f:
+ with open("schema/meta-schema.json") as f:
actual = json.load(f)
- assert actual == get_metaschema(), 'Run: python manage.py pre-commit'
+ assert actual == get_metaschema(), "Run: python manage.py pre-commit"
From 768820a98347587cd312437630639f82c8b178d6 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Tue, 26 Nov 2024 23:17:58 -0500
Subject: [PATCH 18/47] chore: Run pre-commit run --all-files
(cherry picked from commit b7aaa4b03175d59121525f32b50ae635619a8698)
---
common-requirements.txt | 2 +-
manage.py | 439 ++++++++++++++++++++--------------------
2 files changed, 223 insertions(+), 218 deletions(-)
diff --git a/common-requirements.txt b/common-requirements.txt
index 9dd5cffd2..3076effd9 100644
--- a/common-requirements.txt
+++ b/common-requirements.txt
@@ -158,7 +158,7 @@ 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
+standard-theme @ git+https://github.com/open-contracting/standard_theme.git@c3cbd1b8ba6db24624e5d838ed18147db63f5d1b#egg=standard_theme
# via -r common-requirements.in
starlette==0.40.0
# via sphinx-autobuild
diff --git a/manage.py b/manage.py
index 7da07066a..6a31fc1e1 100755
--- a/manage.py
+++ b/manage.py
@@ -24,16 +24,16 @@
from lxml import etree
basedir = Path(__file__).resolve().parent
-schemadir = basedir / 'schema'
-localedir = basedir / 'docs' / 'locale'
+schemadir = basedir / "schema"
+localedir = basedir / "docs" / "locale"
-sys.path.append(str(basedir / 'docs'))
+sys.path.append(str(basedir / "docs"))
from conf import release # noqa: E402
def custom_warning_formatter(message, category, filename, lineno, line=None):
- return str(message) + '\n'
+ return str(message) + "\n"
warnings.formatwarning = custom_warning_formatter
@@ -64,54 +64,49 @@ def custom_warning_formatter(message, category, filename, lineno, line=None):
""")
common_versioned_definitions = {
- 'StringNullUriVersioned': {
- 'type': ['string', 'null'],
- 'format': 'uri',
+ "StringNullUriVersioned": {
+ "type": ["string", "null"],
+ "format": "uri",
},
- 'StringNullDateTimeVersioned': {
- 'type': ['string', 'null'],
- 'format': 'date-time',
+ "StringNullDateTimeVersioned": {
+ "type": ["string", "null"],
+ "format": "date-time",
},
- 'StringNullVersioned': {
- 'type': ['string', 'null'],
- 'format': None,
+ "StringNullVersioned": {
+ "type": ["string", "null"],
+ "format": None,
},
}
recognized_types = (
# Array
- ['array'],
- ['array', 'null'], # optional string arrays
-
+ ["array"],
+ ["array", "null"], # optional string arrays
# Object
- ['object'],
- ['object', 'null'], # /Organization/details
-
+ ["object"],
+ ["object", "null"], # /Organization/details
# String
- ['string'],
- ['string', 'null'],
-
+ ["string"],
+ ["string", "null"],
# Literal
- ['boolean', 'null'],
- ['integer', 'null'],
- ['number', 'null'],
-
+ ["boolean", "null"],
+ ["integer", "null"],
+ ["number", "null"],
# Mixed
- ['string', 'integer'],
- ['string', 'integer', 'null'],
+ ["string", "integer"],
+ ["string", "integer", "null"],
)
keywords_to_remove = (
# Metadata keywords
# https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6
- 'title',
- 'description',
- 'default',
-
+ "title",
+ "description",
+ "default",
# Extended keywords
# http://os4d.opendataservices.coop/development/schema/#extended-json-schema
- 'omitWhenMerged',
- 'wholeListMerge',
+ "omitWhenMerged",
+ "wholeListMerge",
)
@@ -123,12 +118,12 @@ def json_load(filename, library=json, **kwargs):
def json_dump(filename, data):
"""Write JSON data to the given filename."""
- with (schemadir / filename).open('w') as f:
+ with (schemadir / filename).open("w") as f:
json.dump(data, f, indent=2)
- f.write('\n')
+ f.write("\n")
-def csv_load(url, delimiter=','):
+def csv_load(url, delimiter=","):
"""Load CSV data into a ``csv.DictReader`` from the given URL."""
return csv.DictReader(StringIO(get(url).text), delimiter=delimiter)
@@ -136,8 +131,8 @@ def csv_load(url, delimiter=','):
@contextmanager
def csv_dump(filename, fieldnames):
"""Write CSV headers to the given filename, and yield a ``csv.writer``."""
- f = (schemadir / 'codelists' / filename).open('w')
- writer = csv.writer(f, lineterminator='\n')
+ f = (schemadir / "codelists" / filename).open("w")
+ writer = csv.writer(f, lineterminator="\n")
writer.writerow(fieldnames)
try:
yield writer
@@ -162,8 +157,9 @@ def coerce_to_list(data, key):
def get_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'))
+ 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):
@@ -178,18 +174,18 @@ def get_common_definition_ref(item):
# And adds no keywords to the definition.
if any(keyword not in {*keywords, *keywords_to_remove} for keyword in item):
continue
- return {'$ref': f'#/definitions/{name}'}
+ return {"$ref": f"#/definitions/{name}"}
return None
-def add_versioned(schema, unversioned_pointers, pointer=''):
+def add_versioned(schema, unversioned_pointers, pointer=""):
"""Call ``_add_versioned`` on each field."""
- for key, value in schema['properties'].items():
- new_pointer = f'{pointer}/properties/{key}'
+ for key, value in schema["properties"].items():
+ new_pointer = f"{pointer}/properties/{key}"
_add_versioned(schema, unversioned_pointers, new_pointer, key, value)
- for key, value in schema.get('definitions', {}).items():
- new_pointer = f'{pointer}/definitions/{key}'
+ for key, value in schema.get("definitions", {}).items():
+ new_pointer = f"{pointer}/definitions/{key}"
add_versioned(value, unversioned_pointers, pointer=new_pointer)
@@ -207,98 +203,98 @@ def _add_versioned(schema, unversioned_pointers, pointer, key, value):
if pointer in unversioned_pointers:
return
- types = coerce_to_list(value, 'type')
+ types = coerce_to_list(value, "type")
# If a type is unrecognized, we might need to update this script.
- if '$ref' not in value and types not in recognized_types:
- warnings.warn(f'{pointer} has unrecognized type {types}')
+ if "$ref" not in value and types not in recognized_types:
+ warnings.warn(f"{pointer} has unrecognized type {types}")
# For example, if $ref is used.
if not types:
# Ignore the `amendment` field, which had no `id` field in OCDS 1.0.
- if 'deprecated' not in value:
+ if "deprecated" not in value:
versioned_pointer = f"{value['$ref'][1:]}/properties/id"
# If the `id` field is on an object not in an array, it needs to be versioned (e.g. buyer/properties/id).
if versioned_pointer in unversioned_pointers:
- value['$ref'] = value['$ref'] + 'VersionedId'
+ value["$ref"] = value["$ref"] + "VersionedId"
return
# Reference a common versioned definition if possible, to limit the size of the schema.
ref = get_common_definition_ref(value)
if ref:
- schema['properties'][key] = ref
+ schema["properties"][key] = ref
# Iterate into objects with properties like `Item.unit`. Otherwise, version objects with no properties as a
# whole, like `Organization.details`.
- elif types == ['object'] and 'properties' in value:
+ elif types == ["object"] and "properties" in value:
add_versioned(value, unversioned_pointers, pointer=pointer)
else:
new_value = deepcopy(value)
- if types == ['array']:
- item_types = coerce_to_list(value['items'], 'type')
+ if types == ["array"]:
+ item_types = coerce_to_list(value["items"], "type")
# See https://standard.open-contracting.org/latest/en/schema/merging/#whole-list-merge
- if value.get('wholeListMerge'):
+ if value.get("wholeListMerge"):
# Update `$ref` to the unversioned definition.
- if '$ref' in value['items']:
- new_value['items']['$ref'] = value['items']['$ref'] + 'Unversioned'
+ if "$ref" in value["items"]:
+ new_value["items"]["$ref"] = value["items"]["$ref"] + "Unversioned"
# Otherwise, similarly, don't iterate over item properties.
# See https://standard.open-contracting.org/latest/en/schema/merging/#lists
- elif '$ref' in value['items']:
+ elif "$ref" in value["items"]:
# Leave `$ref` to the versioned definition.
return
# Exceptional case for deprecated `Amendment.changes`.
- elif item_types == ['object'] and pointer == '/definitions/Amendment/properties/changes':
+ elif item_types == ["object"] and pointer == "/definitions/Amendment/properties/changes":
return
# Warn in case new combinations are added to the release schema.
- elif item_types != ['string']:
+ elif item_types != ["string"]:
# Note: Versioning the properties of un-$ref'erenced objects in arrays isn't implemented. However,
# this combination hasn't occurred, with the exception of `Amendment/changes`.
warnings.warn(f"{pointer}/items has unexpected type {item_types}")
versioned = deepcopy(versioned_template)
- versioned['items']['properties']['value'] = new_value
- schema['properties'][key] = versioned
+ versioned["items"]["properties"]["value"] = new_value
+ schema["properties"][key] = versioned
def update_refs_to_unversioned_definitions(schema):
"""Replace ``$ref`` values with unversioned definitions."""
for key, value in schema.items():
- if key == '$ref':
- schema[key] = value + 'Unversioned'
+ if key == "$ref":
+ schema[key] = value + "Unversioned"
elif isinstance(value, dict):
update_refs_to_unversioned_definitions(value)
-def get_unversioned_pointers(schema, fields, pointer=''):
+def get_unversioned_pointers(schema, fields, pointer=""):
"""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}')
+ get_unversioned_pointers(item, fields, pointer=f"{pointer}/{index}")
elif isinstance(schema, dict):
# Follows the logic of _get_merge_rules in merge.py from ocds-merge.
- types = coerce_to_list(schema, 'type')
+ types = coerce_to_list(schema, "type")
# If an array is whole list merge, its items are unversioned.
- if 'array' in types and schema.get('wholeListMerge'):
+ if "array" in types and schema.get("wholeListMerge"):
return
- if 'array' in types and 'items' in schema:
- item_types = coerce_to_list(schema['items'], 'type')
+ if "array" in types and "items" in schema:
+ item_types = coerce_to_list(schema["items"], "type")
# If an array mixes objects and non-objects, it is whole list merge.
- if any(item_type != 'object' for item_type in item_types):
+ if any(item_type != "object" for item_type in item_types):
return
# If it is an array of objects, any `id` fields are unversioned.
- if 'id' in schema['items']['properties']:
- if hasattr(schema['items'], '__reference__'):
- reference = schema['items'].__reference__['$ref'][1:]
+ if "id" in schema["items"]["properties"]:
+ if hasattr(schema["items"], "__reference__"):
+ reference = schema["items"].__reference__["$ref"][1:]
else:
reference = pointer
- fields.add(f'{reference}/properties/id')
+ fields.add(f"{reference}/properties/id")
for key, value in schema.items():
- get_unversioned_pointers(value, fields, pointer=f'{pointer}/{key}')
+ get_unversioned_pointers(value, fields, pointer=f"{pointer}/{key}")
def remove_omit_when_merged(schema):
@@ -308,12 +304,12 @@ def remove_omit_when_merged(schema):
remove_omit_when_merged(item)
elif isinstance(schema, dict):
for key, value in schema.items():
- if key == 'properties':
+ if key == "properties":
for prop in list(value):
- if value[prop].get('omitWhenMerged'):
+ if value[prop].get("omitWhenMerged"):
del value[prop]
- if prop in schema['required']:
- schema['required'].remove(prop)
+ if prop in schema["required"]:
+ schema["required"].remove(prop)
remove_omit_when_merged(value)
@@ -324,7 +320,7 @@ def remove_metadata_and_extended_keywords(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)
@@ -334,15 +330,17 @@ def remove_metadata_and_extended_keywords(schema):
def get_versioned_release_schema(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'
- schema['title'] = 'Schema for a compiled, versioned Open Contracting Release.'
+ release_with_underscores = release.replace(".", "__")
+ 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.
remove_omit_when_merged(schema)
# Create unversioned copies of all definitions.
- unversioned_definitions = {k + 'Unversioned': deepcopy(v) for k, v in schema['definitions'].items()}
+ unversioned_definitions = {k + "Unversioned": deepcopy(v) for k, v in schema["definitions"].items()}
update_refs_to_unversioned_definitions(unversioned_definitions)
# Determine which `id` fields occur on objects in arrays.
@@ -350,17 +348,17 @@ def get_versioned_release_schema(schema):
get_unversioned_pointers(jsonref.replace_refs(schema), unversioned_pointers)
# Omit `ocid` from versioning.
- ocid = schema['properties'].pop('ocid')
+ ocid = schema["properties"].pop("ocid")
add_versioned(schema, unversioned_pointers)
- schema['properties']['ocid'] = ocid
+ schema["properties"]["ocid"] = ocid
# Add the common versioned definitions.
for name, keywords in common_versioned_definitions.items():
versioned = deepcopy(versioned_template)
for keyword, value in keywords.items():
if value:
- versioned['items']['properties']['value'][keyword] = value
- schema['definitions'][name] = versioned
+ versioned["items"]["properties"]["value"][keyword] = value
+ schema["definitions"][name] = versioned
# Add missing definitions.
while True:
@@ -370,17 +368,17 @@ def get_versioned_release_schema(schema):
except jsonref.JsonRefError as e:
name = e.cause.args[0]
- if name.endswith('VersionedId'):
+ if name.endswith("VersionedId"):
# Add a copy of an definition with a versioned `id` field, using the same logic as before.
- definition = deepcopy(schema['definitions'][name[:-11]])
- pointer = f'/definitions/{name[:-11]}/properties/id'
+ definition = deepcopy(schema["definitions"][name[:-11]])
+ pointer = f"/definitions/{name[:-11]}/properties/id"
pointers = unversioned_pointers - {pointer}
- _add_versioned(definition, pointers, pointer, 'id', definition['properties']['id'])
+ _add_versioned(definition, pointers, pointer, "id", definition["properties"]["id"])
else:
# Add a copy of an definition with no versioned fields.
definition = unversioned_definitions[name]
- schema['definitions'][name] = definition
+ schema["definitions"][name] = definition
# Remove all metadata and extended keywords.
remove_metadata_and_extended_keywords(schema)
@@ -394,7 +392,7 @@ def cli():
@cli.command()
-@click.argument('filename')
+@click.argument("filename")
def unused_terms(filename):
"""
Print terms in FILENAME that don't occur in the documentation.
@@ -402,75 +400,80 @@ def unused_terms(filename):
Can be used to remove unused terms from a glossary.
"""
paths = []
- for extension in ('csv', 'json', 'md'):
- paths.extend(glob(str(basedir / 'docs' / '**' / f'*.{extension}'), recursive=True))
+ for extension in ("csv", "json", "md"):
+ paths.extend(glob(str(basedir / "docs" / "**" / f"*.{extension}"), recursive=True))
corpus = []
for path in paths:
with open(path) as f:
# Replace punctuation with whitespace, except in abbreviations like "e.g.".
- corpus.append(re.sub(r'(? historic_codes[code]['Valid Until'])
+ and (code not in historic_codes or valid_until > historic_codes[code]["Valid Until"])
):
- historic_codes[code] = {'Title': title, 'Valid Until': valid_until}
+ historic_codes[code] = {"Title": title, "Valid Until": valid_until}
- with csv_dump('currency.csv', ['Code', 'Title', 'Valid Until']) as writer:
+ with csv_dump("currency.csv", ["Code", "Title", "Valid Until"]) as writer:
for code in sorted(current_codes):
writer.writerow([code, current_codes[code], None])
for code in sorted(historic_codes):
- writer.writerow([code, historic_codes[code]['Title'], historic_codes[code]['Valid Until']])
+ writer.writerow([code, historic_codes[code]["Title"], historic_codes[code]["Valid Until"]])
- release_schema = json_load('release-schema.json')
+ release_schema = json_load("release-schema.json")
codes = sorted([*current_codes, historic_codes])
- release_schema['definitions']['Value']['properties']['currency']['enum'] = [*codes, None]
+ release_schema["definitions"]["Value"]["properties"]["currency"]["enum"] = [*codes, None]
- json_dump('release-schema.json', release_schema)
+ json_dump("release-schema.json", release_schema)
@cli.command()
@@ -592,14 +594,14 @@ def update_language():
# 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:
- for row in csv_load('https://id.loc.gov/vocabulary/iso639-1.tsv', delimiter='\t'):
+ with csv_dump("language.csv", ["Code", "Title"]) as writer:
+ 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)']))
+ 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
# a pipe. To preserve order, a dict without values is used instead of a set.
- titles = ', '.join({' '.join(reversed(title.split(', '))): None for title in titles})
- writer.writerow([row['code'], titles])
+ titles = ", ".join({" ".join(reversed(title.split(", "))): None for title in titles})
+ writer.writerow([row["code"], titles])
@cli.command()
@@ -613,37 +615,37 @@ def update_media_type():
# See "Registries included below".
registries = [
- 'application',
- 'audio',
- 'font',
- 'image',
- 'message',
- 'model',
- 'multipart',
- 'text',
- 'video',
+ "application",
+ "audio",
+ "font",
+ "image",
+ "message",
+ "model",
+ "multipart",
+ "text",
+ "video",
]
- with csv_dump('mediaType.csv', ['Code', 'Title']) as writer:
+ with csv_dump("mediaType.csv", ["Code", "Title"]) as writer:
for registry in registries:
# See "Available Formats" under each heading.
- 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)
+ 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:
- name, message = row['Name'], None
+ name, message = row["Name"], None
code = f"{registry}/{name}"
- template = row['Template']
+ template = row["Template"]
# All messages are expected to be about deprecation and obsoletion.
if message:
- logging.warning('%s: %s', message, code)
+ 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 click.ClickException(f"expected {code}, got {template}")
else:
writer.writerow([code, name])
- writer.writerow(['offline/print', 'print'])
+ writer.writerow(["offline/print", "print"])
@cli.command()
@@ -659,7 +661,7 @@ def update(ctx):
@click.pass_context
def check_iso_6523(ctx):
def text(node, xpath):
- return re.sub(r'\s+', ' ', node.xpath(xpath)[0])
+ return re.sub(r"\s+", " ", node.xpath(xpath)[0])
"""
Checks PEPPOL BIS Billing 3.0's ISO 6523 ICD codelist for new codes.
@@ -671,52 +673,54 @@ def text(node, xpath):
maximum = 213
skipped = {92, 103, 181, 182}
- response = get('https://docs.peppol.eu/poacc/billing/3.0/codelist/ICD/')
+ response = get("https://docs.peppol.eu/poacc/billing/3.0/codelist/ICD/")
- divs = lxml.html.fromstring(response.content).xpath('//dd/div[@id]')
+ divs = lxml.html.fromstring(response.content).xpath("//dd/div[@id]")
if not divs:
- raise click.ClickException('The HTML markup of the data source has changed. Please update the script.')
+ raise click.ClickException("The HTML markup of the data source has changed. Please update the script.")
rows = []
for div in divs:
- identifier = div.attrib['id']
+ identifier = div.attrib["id"]
number = int(identifier)
if number < minimum or number > maximum or number in skipped:
- name = text(div, './strong/text()')
- notes = text(div, './p/text()')
- issuer = ''
+ name = text(div, "./strong/text()")
+ notes = text(div, "./p/text()")
+ issuer = ""
# "Issuing agency: " appears at the end of the paragraph. The rest of the paragraph contains either a
# purpose ("Intended Purpose/App. Area") or notes ("Notes on Use of Code"), with or without the label.
- notes = re.sub(r'(Notes on Use of Code|Intended Purpose/App. Area)[: ]+', '', notes)
- if 'Issuing agency: ' in notes:
- notes, issuer = notes.split('Issuing agency: ')
+ notes = re.sub(r"(Notes on Use of Code|Intended Purpose/App. Area)[: ]+", "", notes)
+ if "Issuing agency: " in notes:
+ notes, issuer = notes.split("Issuing agency: ")
rows.append([identifier, name, issuer, notes])
if rows:
- csv.writer(sys.stdout, delimiter='\t').writerows(rows)
+ csv.writer(sys.stdout, delimiter="\t").writerows(rows)
else:
- click.echo('No new codes found.')
+ click.echo("No new codes found.")
def add_translation_note(path, language, domain):
"""Add a translation note to a file."""
- base_url = 'https://standard.open-contracting.org/1.1'
+ base_url = "https://standard.open-contracting.org/1.1"
with open(path) as f:
document = lxml.html.fromstring(f.read())
- translator = gettext.translation('theme', localedir, languages=[language])
+ translator = gettext.translation("theme", localedir, languages=[language])
_ = translator.gettext
- pattern = f'{base_url}/{{}}/{domain}/'
+ pattern = f"{base_url}/{{}}/{domain}/"
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 == requests.codes.not_found:
- message = _('This page was recently added to the English documentation. '
- 'It has not yet been translated.')
+ message = _(
+ 'This page was recently added to the English documentation. '
+ "It has not yet been translated."
+ )
# If it's an existing page, add the note the last version of the page.
else:
@@ -724,10 +728,10 @@ def add_translation_note(path, language, domain):
xpath = '//div[@itemprop="articleBody"]'
replacement = lxml.html.fromstring(response.content).xpath(xpath)[0]
- replacement.make_links_absolute(f'{base_url}/{language}')
+ replacement.make_links_absolute(f"{base_url}/{language}")
# Remove any existing translation notes.
- parent = replacement.xpath('//h1')[0].getparent()
+ parent = replacement.xpath("//h1")[0].getparent()
for div in replacement.xpath('//h1/following-sibling::div[@class="admonition note"]'):
parent.remove(div)
@@ -736,19 +740,20 @@ def add_translation_note(path, language, domain):
message = _(
'This page was recently changed in the English documentation. '
- 'The changes have not yet been translated.'
+ "The changes have not yet been translated."
)
template = (
''
+ "%(message)s
"
)
- document.xpath('//h1')[0].addnext(lxml.etree.XML(template % {
- 'note': _('Note'), 'message': message % {'url': pattern.format('en')}}))
+ document.xpath("//h1")[0].addnext(
+ lxml.etree.XML(template % {"note": _("Note"), "message": message % {"url": pattern.format("en")}})
+ )
- with open(path, 'wb') as f:
- f.write(lxml.html.tostring(document, encoding='utf-8'))
+ with open(path, "wb") as f:
+ f.write(lxml.html.tostring(document, encoding="utf-8"))
@cli.command()
@@ -762,11 +767,11 @@ def add_translation_notes():
https://standard.open-contracting.org/1.1/en/governance/#translation-and-localization-policy
"""
- excluded = ('.doctrees', '_downloads', '_images', '_sources', '_static', 'codelists', 'genindex', 'search')
+ excluded = (".doctrees", "_downloads", "_images", "_sources", "_static", "codelists", "genindex", "search")
- for language in ('es', 'fr'):
- build_dir = basedir / 'build' / language
- language_dir = localedir / language / 'LC_MESSAGES'
+ for language in ("es", "fr"):
+ build_dir = basedir / "build" / language
+ language_dir = localedir / language / "LC_MESSAGES"
for root, dirs, files in os.walk(build_dir):
# Skip Sphinx directories.
@@ -783,9 +788,9 @@ def add_translation_notes():
source = os.path.join(root, os.path.dirname(name))
domain = relative_path(build_dir, source)
- path = language_dir / domain / 'index.po'
+ path = language_dir / domain / "index.po"
if not path.is_file():
- path = language_dir / f'{domain}.po'
+ path = language_dir / f"{domain}.po"
if not path.is_file():
add_translation_note(os.path.join(root, name), language, domain)
continue
@@ -798,5 +803,5 @@ def add_translation_notes():
break
-if __name__ == '__main__':
+if __name__ == "__main__":
cli()
From a3472ce96d4653a0b2dc0a9f14440abaae5e944a Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Fri, 13 Dec 2024 19:09:03 -0500
Subject: [PATCH 19/47] build: Upgrade ocdsextensionregistry. Remove milestone
documents extension (changing OCDS validations is prohibited).
(cherry picked from commit be321ae26790f006ae622280707a664ee3688fb5)
---
common-requirements.txt | 2 +-
docs/conf.py | 1 -
docs/history/changelog.md | 2 +-
.../es/LC_MESSAGES/history/changelog.po | 6 +-
.../fr/LC_MESSAGES/history/changelog.po | 3 +-
docs/schema/reference.md | 4 -
manage.py | 310 +-----------------
tests/test_schema_integrity.py | 13 +-
8 files changed, 19 insertions(+), 322 deletions(-)
diff --git a/common-requirements.txt b/common-requirements.txt
index 3076effd9..c81779f3c 100644
--- a/common-requirements.txt
+++ b/common-requirements.txt
@@ -90,7 +90,7 @@ myst-parser==0.18.1
# via -r common-requirements.in
ocds-babel==0.3.6
# via -r common-requirements.in
-ocdsextensionregistry==0.5.0
+ocdsextensionregistry==0.6.0
# via -r common-requirements.in
ocdsindex==0.2.0
# via -r common-requirements.in
diff --git a/docs/conf.py b/docs/conf.py
index 5522f2259..ede1ba262 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -114,7 +114,6 @@
"enquiries": default_extension_version,
"location": default_extension_version,
"lots": default_extension_version,
- "milestone_documents": default_extension_version,
"participation_fee": default_extension_version,
"process_title": default_extension_version,
}
diff --git a/docs/history/changelog.md b/docs/history/changelog.md
index de29b17fd..186e72a16 100644
--- a/docs/history/changelog.md
+++ b/docs/history/changelog.md
@@ -109,7 +109,7 @@ See the changelogs for:
* [Enquiries](https://extensions.open-contracting.org/en/extensions/enquiries/master/#changelog)
* [Location](https://extensions.open-contracting.org/en/extensions/location/master/#changelog)
* [Lots](https://extensions.open-contracting.org/en/extensions/lots/master/#changelog)
-* [Milestone documents](https://extensions.open-contracting.org/en/extensions/milestone_documents/master/#changelog)
+* [Milestone documents](https://github.com/open-contracting-extensions/ocds_milestone_documents_extension#changelog)
* [Participation fees](https://extensions.open-contracting.org/en/extensions/participation_fee/master/#changelog)
* [Process level title and description](https://extensions.open-contracting.org/en/extensions/process_title/master/#changelog)
diff --git a/docs/locale/es/LC_MESSAGES/history/changelog.po b/docs/locale/es/LC_MESSAGES/history/changelog.po
index 30c2aa5a8..8aa20aada 100644
--- a/docs/locale/es/LC_MESSAGES/history/changelog.po
+++ b/docs/locale/es/LC_MESSAGES/history/changelog.po
@@ -792,11 +792,9 @@ msgstr ""
#: ../../docs/history/changelog.md:111
msgid ""
-"[Milestone documents](https://extensions.open-"
-"contracting.org/en/extensions/milestone_documents/master/#changelog)"
+"[Milestone documents](https://github.com/open-contracting-extensions/ocds_milestone_documents_extension#changelog)"
msgstr ""
-"[Documentos de hito](https://extensions.open-"
-"contracting.org/en/extensions/milestone_documents/master/#changelog)"
+"[Documentos de hito](https://github.com/open-contracting-extensions/ocds_milestone_documents_extension#changelog)"
#: ../../docs/history/changelog.md:112 ../../docs/history/changelog.md:190
msgid ""
diff --git a/docs/locale/fr/LC_MESSAGES/history/changelog.po b/docs/locale/fr/LC_MESSAGES/history/changelog.po
index 84975e6b1..7e457580e 100644
--- a/docs/locale/fr/LC_MESSAGES/history/changelog.po
+++ b/docs/locale/fr/LC_MESSAGES/history/changelog.po
@@ -601,8 +601,7 @@ msgstr ""
#: ../../docs/history/changelog.md:110
msgid ""
-"[Milestone documents](https://extensions.open-"
-"contracting.org/en/extensions/milestone_documents/master/#changelog)"
+"[Milestone documents](https://github.com/open-contracting-extensions/ocds_milestone_documents_extension#changelog)"
msgstr ""
#: ../../docs/history/changelog.md:111 ../../docs/history/changelog.md:189
diff --git a/docs/schema/reference.md b/docs/schema/reference.md
index 9d7926037..334cc758c 100644
--- a/docs/schema/reference.md
+++ b/docs/schema/reference.md
@@ -402,10 +402,6 @@ Notes:
* The `dateModified` field should be changed whenever the progress towards a milestone is reviewed, and the `status` either updated, or re-confirmed.
-```{extensionlist} The following extensions to milestone are available
-:list: milestones
-```
-
### Value
Financial values should be published with a currency attached.
diff --git a/manage.py b/manage.py
index 6a31fc1e1..2cd49e3a9 100755
--- a/manage.py
+++ b/manage.py
@@ -8,7 +8,6 @@
import sys
import warnings
from contextlib import contextmanager
-from copy import deepcopy
from glob import glob
from io import StringIO
from pathlib import Path
@@ -22,6 +21,7 @@
from babel.messages.pofile import read_po
from docutils.utils import relative_path
from lxml import etree
+from ocdsextensionregistry import get_versioned_release_schema
basedir = Path(__file__).resolve().parent
schemadir = basedir / "schema"
@@ -38,77 +38,6 @@ def custom_warning_formatter(message, category, filename, lineno, line=None):
warnings.formatwarning = custom_warning_formatter
-versioned_template = json.loads("""
-{
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "releaseDate": {
- "format": "date-time",
- "type": "string"
- },
- "releaseID": {
- "type": "string"
- },
- "value": {},
- "releaseTag": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- }
- }
-}
-""")
-
-common_versioned_definitions = {
- "StringNullUriVersioned": {
- "type": ["string", "null"],
- "format": "uri",
- },
- "StringNullDateTimeVersioned": {
- "type": ["string", "null"],
- "format": "date-time",
- },
- "StringNullVersioned": {
- "type": ["string", "null"],
- "format": None,
- },
-}
-
-recognized_types = (
- # Array
- ["array"],
- ["array", "null"], # optional string arrays
- # Object
- ["object"],
- ["object", "null"], # /Organization/details
- # String
- ["string"],
- ["string", "null"],
- # Literal
- ["boolean", "null"],
- ["integer", "null"],
- ["number", "null"],
- # Mixed
- ["string", "integer"],
- ["string", "integer", "null"],
-)
-
-keywords_to_remove = (
- # Metadata keywords
- # https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6
- "title",
- "description",
- "default",
- # Extended keywords
- # http://os4d.opendataservices.coop/development/schema/#extended-json-schema
- "omitWhenMerged",
- "wholeListMerge",
-)
-
def json_load(filename, library=json, **kwargs):
"""Load JSON data from the given filename."""
@@ -147,14 +76,6 @@ def get(url):
return response
-def coerce_to_list(data, key):
- """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]
- return item
-
-
def get_metaschema():
"""Patches and returns the JSON Schema Draft 4 metaschema."""
return json_merge_patch.merge(
@@ -162,230 +83,6 @@ def get_metaschema():
)
-def get_common_definition_ref(item):
- """
- Return a schema that references the common definition that the ``item`` matches: "StringNullUriVersioned",
- "StringNullDateTimeVersioned" or "StringNullVersioned".
- """
- for name, keywords in common_versioned_definitions.items():
- # If the item matches the definition.
- 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):
- continue
- return {"$ref": f"#/definitions/{name}"}
- return None
-
-
-def add_versioned(schema, unversioned_pointers, pointer=""):
- """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)
-
- for key, value in schema.get("definitions", {}).items():
- new_pointer = f"{pointer}/definitions/{key}"
- add_versioned(value, unversioned_pointers, pointer=new_pointer)
-
-
-def _add_versioned(schema, unversioned_pointers, pointer, key, value):
- """
- 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
- :param pointer str: the field's pointer
- :param key str: the field's name
- :param value str: the field's schema
- """
- # Skip unversioned fields.
- if pointer in unversioned_pointers:
- return
-
- types = coerce_to_list(value, "type")
-
- # If a type is unrecognized, we might need to update this script.
- if "$ref" not in value and types not in recognized_types:
- warnings.warn(f"{pointer} has unrecognized type {types}")
-
- # For example, if $ref is used.
- if not types:
- # Ignore the `amendment` field, which had no `id` field in OCDS 1.0.
- if "deprecated" not in value:
- versioned_pointer = f"{value['$ref'][1:]}/properties/id"
- # If the `id` field is on an object not in an array, it needs to be versioned (e.g. buyer/properties/id).
- if versioned_pointer in unversioned_pointers:
- value["$ref"] = value["$ref"] + "VersionedId"
- return
-
- # Reference a common versioned definition if possible, to limit the size of the schema.
- ref = get_common_definition_ref(value)
- if ref:
- schema["properties"][key] = ref
-
- # Iterate into objects with properties like `Item.unit`. Otherwise, version objects with no properties as a
- # whole, like `Organization.details`.
- elif types == ["object"] and "properties" in value:
- add_versioned(value, unversioned_pointers, pointer=pointer)
-
- else:
- new_value = deepcopy(value)
-
- if types == ["array"]:
- item_types = coerce_to_list(value["items"], "type")
-
- # See https://standard.open-contracting.org/latest/en/schema/merging/#whole-list-merge
- if value.get("wholeListMerge"):
- # Update `$ref` to the unversioned definition.
- if "$ref" in value["items"]:
- new_value["items"]["$ref"] = value["items"]["$ref"] + "Unversioned"
- # Otherwise, similarly, don't iterate over item properties.
- # See https://standard.open-contracting.org/latest/en/schema/merging/#lists
- elif "$ref" in value["items"]:
- # Leave `$ref` to the versioned definition.
- return
- # Exceptional case for deprecated `Amendment.changes`.
- elif item_types == ["object"] and pointer == "/definitions/Amendment/properties/changes":
- return
- # Warn in case new combinations are added to the release schema.
- elif item_types != ["string"]:
- # Note: Versioning the properties of un-$ref'erenced objects in arrays isn't implemented. However,
- # this combination hasn't occurred, with the exception of `Amendment/changes`.
- warnings.warn(f"{pointer}/items has unexpected type {item_types}")
-
- versioned = deepcopy(versioned_template)
- versioned["items"]["properties"]["value"] = new_value
- schema["properties"][key] = versioned
-
-
-def update_refs_to_unversioned_definitions(schema):
- """Replace ``$ref`` values with unversioned definitions."""
- for key, value in schema.items():
- if key == "$ref":
- schema[key] = value + "Unversioned"
- elif isinstance(value, dict):
- update_refs_to_unversioned_definitions(value)
-
-
-def get_unversioned_pointers(schema, fields, pointer=""):
- """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}")
- elif isinstance(schema, dict):
- # Follows the logic of _get_merge_rules in merge.py from ocds-merge.
- types = coerce_to_list(schema, "type")
-
- # If an array is whole list merge, its items are unversioned.
- if "array" in types and schema.get("wholeListMerge"):
- return
- if "array" in types and "items" in schema:
- item_types = coerce_to_list(schema["items"], "type")
- # If an array mixes objects and non-objects, it is whole list merge.
- if any(item_type != "object" for item_type in item_types):
- return
- # If it is an array of objects, any `id` fields are unversioned.
- if "id" in schema["items"]["properties"]:
- if hasattr(schema["items"], "__reference__"):
- reference = schema["items"].__reference__["$ref"][1:]
- else:
- reference = pointer
- fields.add(f"{reference}/properties/id")
-
- for key, value in schema.items():
- get_unversioned_pointers(value, fields, pointer=f"{pointer}/{key}")
-
-
-def remove_omit_when_merged(schema):
- """Remove properties that set ``omitWhenMerged``."""
- if isinstance(schema, list):
- for item in schema:
- remove_omit_when_merged(item)
- elif isinstance(schema, dict):
- for key, value in schema.items():
- if key == "properties":
- for prop in list(value):
- if value[prop].get("omitWhenMerged"):
- del value[prop]
- if prop in schema["required"]:
- schema["required"].remove(prop)
- remove_omit_when_merged(value)
-
-
-def remove_metadata_and_extended_keywords(schema):
- """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"}:
- for subschema in value.values():
- for keyword in keywords_to_remove:
- subschema.pop(keyword, None)
- remove_metadata_and_extended_keywords(value)
-
-
-def get_versioned_release_schema(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"
- )
- schema["title"] = "Schema for a compiled, versioned Open Contracting Release."
-
- # Release IDs, dates and tags appear alongside values in the versioned release schema.
- remove_omit_when_merged(schema)
-
- # Create unversioned copies of all definitions.
- unversioned_definitions = {k + "Unversioned": deepcopy(v) for k, v in schema["definitions"].items()}
- update_refs_to_unversioned_definitions(unversioned_definitions)
-
- # Determine which `id` fields occur on objects in arrays.
- unversioned_pointers = set()
- get_unversioned_pointers(jsonref.replace_refs(schema), unversioned_pointers)
-
- # Omit `ocid` from versioning.
- ocid = schema["properties"].pop("ocid")
- add_versioned(schema, unversioned_pointers)
- schema["properties"]["ocid"] = ocid
-
- # Add the common versioned definitions.
- for name, keywords in common_versioned_definitions.items():
- versioned = deepcopy(versioned_template)
- for keyword, value in keywords.items():
- if value:
- versioned["items"]["properties"]["value"][keyword] = value
- schema["definitions"][name] = versioned
-
- # Add missing definitions.
- while True:
- try:
- jsonref.replace_refs(schema, lazy_load=False)
- break
- except jsonref.JsonRefError as e:
- name = e.cause.args[0]
-
- if name.endswith("VersionedId"):
- # Add a copy of an definition with a versioned `id` field, using the same logic as before.
- definition = deepcopy(schema["definitions"][name[:-11]])
- pointer = f"/definitions/{name[:-11]}/properties/id"
- pointers = unversioned_pointers - {pointer}
- _add_versioned(definition, pointers, pointer, "id", definition["properties"]["id"])
- else:
- # Add a copy of an definition with no versioned fields.
- definition = unversioned_definitions[name]
-
- schema["definitions"][name] = definition
-
- # Remove all metadata and extended keywords.
- remove_metadata_and_extended_keywords(schema)
-
- return schema
-
-
@click.group()
def cli():
pass
@@ -494,7 +191,10 @@ def pre_commit():
json_dump("meta-schema.json", get_metaschema())
json_dump("dereferenced-release-schema.json", jsonref_release_schema)
- json_dump("versioned-release-validation-schema.json", get_versioned_release_schema(release_schema))
+ json_dump(
+ "versioned-release-validation-schema.json",
+ get_versioned_release_schema(release_schema, release.replace(".", "__")),
+ )
@cli.command()
diff --git a/tests/test_schema_integrity.py b/tests/test_schema_integrity.py
index c6fdb0f86..789a27688 100644
--- a/tests/test_schema_integrity.py
+++ b/tests/test_schema_integrity.py
@@ -3,14 +3,19 @@
"""
import json
-import os.path
import sys
+from pathlib import Path
import jsonref
+from ocdsextensionregistry import get_versioned_release_schema
-sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
+basedir = Path(__file__).resolve().parent.parent
-from manage import get_metaschema, get_versioned_release_schema
+sys.path.extend([str(basedir), str(basedir / "docs")])
+
+from conf import release
+
+from manage import get_metaschema
def test_versioned_release_schema_is_in_sync():
@@ -18,7 +23,7 @@ def test_versioned_release_schema_is_in_sync():
actual = json.load(f)
with open("schema/release-schema.json") as f:
- expected = get_versioned_release_schema(json.load(f))
+ expected = get_versioned_release_schema(json.load(f), release.replace(".", "__"))
assert actual == expected, "Run: python manage.py pre-commit"
From 3869581ef6bb3cc1e82b21e3032ecfe4079d80b6 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Fri, 13 Dec 2024 19:12:24 -0500
Subject: [PATCH 20/47] build: Upgrade
sphinxcontrib-opendataservices-jsonschema
(cherry picked from commit 39a7163f781ca15e8d555c78365c72ac9a976131)
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 9c2ea5690..1c7a3ab40 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,5 +5,5 @@
ocdskit==1.1.3
sphinx-design==0.4.1
sphinxcontrib-opencontracting==0.0.8
-sphinxcontrib-opendataservices-jsonschema==0.6.1
+sphinxcontrib-opendataservices-jsonschema==0.7.1
sphinxcontrib-opendataservices==0.5.0
From 80c2efa31bd4dc68752aae96ecb1108fc5add2a6 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Fri, 13 Dec 2024 19:14:20 -0500
Subject: [PATCH 21/47] chore: Add noqa for local imports
(cherry picked from commit 311c9d2fa22f15ce68c964704e4959170a23876d)
---
tests/test_schema_integrity.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/test_schema_integrity.py b/tests/test_schema_integrity.py
index 789a27688..fdbab6b76 100644
--- a/tests/test_schema_integrity.py
+++ b/tests/test_schema_integrity.py
@@ -13,9 +13,9 @@
sys.path.extend([str(basedir), str(basedir / "docs")])
-from conf import release
+from conf import release # noqa: E402
-from manage import get_metaschema
+from manage import get_metaschema # noqa: E402
def test_versioned_release_schema_is_in_sync():
From 51170f85c245e305675fb56c50957140d7def92c Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Fri, 13 Dec 2024 19:19:25 -0500
Subject: [PATCH 22/47] build: Upgrade ocdsextensionregistry
(cherry picked from commit 561a0520508107c3d230ec4c5c206559d281be4d)
---
common-requirements.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/common-requirements.txt b/common-requirements.txt
index c81779f3c..fb836f0ad 100644
--- a/common-requirements.txt
+++ b/common-requirements.txt
@@ -90,7 +90,7 @@ myst-parser==0.18.1
# via -r common-requirements.in
ocds-babel==0.3.6
# via -r common-requirements.in
-ocdsextensionregistry==0.6.0
+ocdsextensionregistry==0.6.1
# via -r common-requirements.in
ocdsindex==0.2.0
# via -r common-requirements.in
@@ -158,7 +158,7 @@ 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@c3cbd1b8ba6db24624e5d838ed18147db63f5d1b#egg=standard_theme
+standard-theme @ git+https://github.com/open-contracting/standard_theme.git@5de343d1d8e342b5f2a42c6132db37aebe382e36#egg=standard_theme
# via -r common-requirements.in
starlette==0.40.0
# via sphinx-autobuild
From a68bf426c99a4dc70f23006192bde09f6d47c49e Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Sun, 15 Dec 2024 02:10:15 -0500
Subject: [PATCH 23/47] build: Upgrade code for latest ocdskit and
ocdsextensionregistry
(cherry picked from commit d2f4a956de5a07398843431a1345a4d8a69f3792)
---
.github/workflows/lint.yml | 2 ++
common-requirements.txt | 2 +-
manage.py | 7 ++++---
requirements.txt | 2 +-
4 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 2f31fb710..600ad9d5a 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -5,5 +5,7 @@ jobs:
uses: open-contracting/.github/.github/workflows/lint.yml@main
permissions:
contents: write
+ secrets:
+ personal-access-token: ${{ secrets.PAT }}
with:
python-version: '3.10'
diff --git a/common-requirements.txt b/common-requirements.txt
index fb836f0ad..3ca01e41a 100644
--- a/common-requirements.txt
+++ b/common-requirements.txt
@@ -90,7 +90,7 @@ myst-parser==0.18.1
# via -r common-requirements.in
ocds-babel==0.3.6
# via -r common-requirements.in
-ocdsextensionregistry==0.6.1
+ocdsextensionregistry==0.6.5
# via -r common-requirements.in
ocdsindex==0.2.0
# via -r common-requirements.in
diff --git a/manage.py b/manage.py
index 2cd49e3a9..f0e6de6b5 100755
--- a/manage.py
+++ b/manage.py
@@ -22,6 +22,7 @@
from docutils.utils import relative_path
from lxml import etree
from ocdsextensionregistry import get_versioned_release_schema
+from ocdsextensionregistry.util import replace_refs
basedir = Path(__file__).resolve().parent
schemadir = basedir / "schema"
@@ -39,10 +40,10 @@ def custom_warning_formatter(message, category, filename, lineno, line=None):
warnings.formatwarning = custom_warning_formatter
-def json_load(filename, library=json, **kwargs):
+def json_load(filename):
"""Load JSON data from the given filename."""
with (schemadir / filename).open() as f:
- return library.load(f, **kwargs)
+ return json.load(f)
def json_dump(filename, data):
@@ -187,7 +188,7 @@ def pre_commit():
- versioned-release-validation-schema.json
"""
release_schema = json_load("release-schema.json")
- jsonref_release_schema = json_load("release-schema.json", jsonref, merge_props=True)
+ jsonref_release_schema = replace_refs(json_load("release-schema.json"), keep_defs=True)
json_dump("meta-schema.json", get_metaschema())
json_dump("dereferenced-release-schema.json", jsonref_release_schema)
diff --git a/requirements.txt b/requirements.txt
index 1c7a3ab40..33d3d98dd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@
# Add your own requirements below.
-ocdskit==1.1.3
+ocdskit==1.3.0
sphinx-design==0.4.1
sphinxcontrib-opencontracting==0.0.8
sphinxcontrib-opendataservices-jsonschema==0.7.1
From 2f528df253d6ad795ea3ce5a08acd2e18ae4d630 Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Sun, 15 Dec 2024 02:20:13 -0500
Subject: [PATCH 24/47] chore: Run ruff check .
(cherry picked from commit 08c8ad03a471335203255b3d439823fb3f00f125)
---
manage.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/manage.py b/manage.py
index f0e6de6b5..b361e3e13 100755
--- a/manage.py
+++ b/manage.py
@@ -14,7 +14,6 @@
import click
import json_merge_patch
-import jsonref
import lxml.etree
import lxml.html
import requests
From 5a7fa46d263f1d1d8a3a97803a7970da7d1210a7 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 6 Jan 2025 21:40:03 +0000
Subject: [PATCH 25/47] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.9 → v0.8.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.9...v0.8.6)
- [github.com/astral-sh/uv-pre-commit: 0.4.18 → 0.5.14](https://github.com/astral-sh/uv-pre-commit/compare/0.4.18...0.5.14)
(cherry picked from commit 4a0a66158562c7d3ce5c799b8d295c1341d830cf)
---
.pre-commit-config.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8c81d5c10..6286a0316 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,12 +5,12 @@ default_language_version:
python: python3.10
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.6.9
+ rev: v0.8.6
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
- rev: 0.4.18
+ rev: 0.5.14
hooks:
- id: pip-compile
name: pip-compile common-requirements.in
From 4c57cb3e4af9fbbf3eaaf8a34ff34a0d802809ad Mon Sep 17 00:00:00 2001
From: James McKinney <26463+jpmckinney@users.noreply.github.com>
Date: Wed, 29 Jan 2025 10:52:48 -0500
Subject: [PATCH 26/47] ui: Update logo
(cherry picked from commit 05e9626a048ef7255208495bf4a68a451908b55c)
---
common-requirements.txt | 4 ++--
docs/_static/basic.css | 9 +++++++++
docs/_static/favicon-16x16.ico | Bin 1406 -> 93062 bytes
docs/_templates/layout.html | 14 +++++---------
include/common.mk | 1 -
include/header.html | 12 ------------
script/update | 2 +-
7 files changed, 17 insertions(+), 25 deletions(-)
delete mode 100644 include/header.html
diff --git a/common-requirements.txt b/common-requirements.txt
index 3ca01e41a..2d408c71d 100644
--- a/common-requirements.txt
+++ b/common-requirements.txt
@@ -64,7 +64,7 @@ imagesize==1.4.1
# via sphinx
iniconfig==1.1.1
# via pytest
-jinja2==3.1.4
+jinja2==3.1.5
# via
# myst-parser
# sphinx
@@ -158,7 +158,7 @@ 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@5de343d1d8e342b5f2a42c6132db37aebe382e36#egg=standard_theme
+standard-theme @ git+https://github.com/open-contracting/standard_theme.git@d56505d69fceeb43535a3387172b2024eead536e#egg=standard_theme
# via -r common-requirements.in
starlette==0.40.0
# via sphinx-autobuild
diff --git a/docs/_static/basic.css b/docs/_static/basic.css
index 86fdf2087..355bdc65a 100644
--- a/docs/_static/basic.css
+++ b/docs/_static/basic.css
@@ -1,3 +1,12 @@
+.wy-side-nav-search {
+ margin: 0;
+ padding: 0;
+}
+.wy-side-nav-search > a, .wy-side-nav-search .wy-dropdown > a {
+ margin: 0;
+ padding: 0 6px;
+}
+
#build-your-data-pipeline li {
margin-bottom: 1em;
}
diff --git a/docs/_static/favicon-16x16.ico b/docs/_static/favicon-16x16.ico
index 40159444df4205bf4b951f36bf7677998df5f7ba..b0139d067154e59e1f327fccf005e2c21e7ae3e1 100644
GIT binary patch
literal 93062
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
literal 1406
zcmeH{-A|HX7{-4H6n=$Tni`2f0z!tpydqc$scA-l#jCcJ9eZN6va+)_w>oOM)ojDQ
z*E*?;YT`$!o2GNknsZ`SYW1J=(7C5=ZQjp*&+}Z@!M)wr!3`>La&i#2j!rEU0Cs_}
zC>5mqjmj%Yc-?NQWf^Z(71cf;9)B&KfFG%$9#4vFgOn2bQi*D$jROdEp$cE0-wkxrpm(H^yt-xOy&A(R-bWo4pj?x`Fe~
zZJc-eF!%T2i1p(bxQF9m4Ev)Pna>AMy}Xa=)dN)V$ATxMy?KiI-80e?FR%^8DI9){
zZgdb^B98j~AlBix=spaRIUc8IVhHQ_2%4!OK?3W9_)jO$P7kB`I70E%7@C<;a%aaV
zo0%lz(u)2hKHA%@@5~IMhwoJ+TGRF0<
zn0~Hc`t=Rd@9!JMz3Fcz@V_UJSFA~mpefw`cM^SF%O0^`be2$Et}wE^iV~G_^scC)
z9LOfqrHG1F`%{l1Wt9dMr8RQ6;!x8`p@SxEfgC#KDR=k+4aYL1r1;qi%H
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index 971c158b6..c68368339 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -4,19 +4,15 @@
{% block mobile_nav %}
-{% trans %} Open Contracting {% endtrans %} {% trans %} Data Standard {% endtrans %} v{{ version }}
+{% trans %}Open Contracting Data Standard{% endtrans %} v{{ version }}
{% endblock %}
{% block sidebartitle %}
-
-