diff --git a/.devcontainer/codeforlife-deploy-appengine/devcontainer.json b/.devcontainer/codeforlife-deploy-appengine/devcontainer.json deleted file mode 100644 index 36521a57..00000000 --- a/.devcontainer/codeforlife-deploy-appengine/devcontainer.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "codeforlife-deploy-appengine", - "dockerComposeFile": [ - "../../docker-compose.yml" - ], - "service": "base-service", - "shutdownAction": "none", - "workspaceFolder": "/workspace/codeforlife-deploy-appengine", - "remoteUser": "root", - "features": { - "ghcr.io/devcontainers/features/python:1": { - "version": "3.8", - "installTools": false - }, - "ghcr.io/devcontainers/features/terraform:1": { - "version": "1.6.6", - "tflint": "none", - "terragrunt": "none" - }, - "ghcr.io/dhoeric/features/google-cloud-cli:1": { - "version": "447.0.0" - } - }, - "customizations": { - "vscode": { - //TODO: Specify preferred settings and extensions once defined - "settings": {}, - "extensions": [ - "github.vscode-pull-request-github", - "redhat.vscode-yaml", - "davidanson.vscode-markdownlint", - "bierner.markdown-mermaid", - "ms-python.python", - "ms-python.pylint", - "ms-python.isort", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "ms-python.black-formatter", - "streetsidesoftware.code-spell-checker", - "tamasfe.even-better-toml", - "kevinrose.vsc-python-indent", - "batisteo.vscode-django", - "njpwerner.autodocstring", - "visualstudioexptteam.vscodeintellicode", - "wholroyd.jinja", - "qwtel.sqlite-viewer" - ] - } - } -} \ No newline at end of file diff --git a/.devcontainer/codeforlife-package-javascript/devcontainer.json b/.devcontainer/codeforlife-package-javascript/devcontainer.json deleted file mode 100644 index 462b1123..00000000 --- a/.devcontainer/codeforlife-package-javascript/devcontainer.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "codeforlife-package-javascript", - "dockerComposeFile": [ - "../../docker-compose.yml" - ], - "service": "base-service", - "shutdownAction": "none", - "workspaceFolder": "/workspace/codeforlife-package-javascript", - "remoteUser": "root", - "postCreateCommand": "yarn install", - "features": { - "ghcr.io/devcontainers/features/node:1": { - "version": "18" - } - }, - "customizations": { - "vscode": { - //TODO: Specify preferred settings and extensions once defined - "settings": {}, - "extensions": [] - } - } -} \ No newline at end of file diff --git a/.devcontainer/codeforlife-package-python/devcontainer.json b/.devcontainer/codeforlife-package-python/devcontainer.json deleted file mode 100644 index 927d3fae..00000000 --- a/.devcontainer/codeforlife-package-python/devcontainer.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "name": "codeforlife-package-python", - "dockerComposeFile": [ - "../../docker-compose.yml" - ], - "service": "base-service", - "shutdownAction": "none", - "workspaceFolder": "/workspace/codeforlife-package-python", - "remoteUser": "root", - "postCreateCommand": "pipenv install --dev", - "features": { - "ghcr.io/devcontainers/features/python:1": { - "version": "3.8", - "installTools": false - }, - "ghcr.io/devcontainers-contrib/features/pipenv:2": { - "version": "2023.11.15" - } - }, - "customizations": { - "vscode": { - //TODO: Specify preferred settings and extensions once defined - "settings": { - "python.defaultInterpreterPath": ".venv/bin/python", - "autoDocstring.customTemplatePath": ".vscode/extensions/autoDocstring/docstring.mustache", - "files.exclude": { - "**/__pycache__": true, - "**/.pytest_cache": true, - "**/.mypy_cache": true, - "**/.hypothesis": true - }, - "editor.tabSize": 2, - "editor.rulers": [ - 80 - ], - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" - }, - "workbench.colorCustomizations": { - "editorRuler.foreground": "#008000" - }, - "[md]": { - "editor.tabSize": 4 - }, - "[python]": { - "editor.tabSize": 4, - "editor.defaultFormatter": "ms-python.black-formatter", - }, - "cSpell.words": [ - "codeforlife", - "klass", - "ocado", - "kurono", - "pipenv" - ] - }, - "extensions": [ - "github.vscode-pull-request-github", - "redhat.vscode-yaml", - "davidanson.vscode-markdownlint", - "bierner.markdown-mermaid", - "ms-python.python", - "ms-python.pylint", - "ms-python.isort", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "ms-python.black-formatter", - "streetsidesoftware.code-spell-checker", - "tamasfe.even-better-toml", - "kevinrose.vsc-python-indent", - "batisteo.vscode-django", - "njpwerner.autodocstring", - "visualstudioexptteam.vscodeintellicode", - "wholroyd.jinja", - "qwtel.sqlite-viewer" - ] - } - } -} \ No newline at end of file diff --git a/.devcontainer/codeforlife-portal-react/devcontainer.json b/.devcontainer/codeforlife-portal-react/devcontainer.json deleted file mode 100644 index 48c8cc2f..00000000 --- a/.devcontainer/codeforlife-portal-react/devcontainer.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "name": "codeforlife-portal-react", - "dockerComposeFile": [ - "../../docker-compose.yml" - ], - "service": "base-service", - "shutdownAction": "none", - "workspaceFolder": "/workspace/codeforlife-portal-react", - "remoteUser": "root", - "postCreateCommand": "./setup", - "features": { - "ghcr.io/devcontainers/features/python:1": { - "version": "3.8", - "installTools": false - }, - "ghcr.io/devcontainers-contrib/features/pipenv:2": { - "version": "2023.11.15" - }, - "ghcr.io/devcontainers/features/node:1": { - "version": "18" - } - }, - "customizations": { - "vscode": { - //TODO: Specify preferred settings and extensions once defined - "settings": { - "python.defaultInterpreterPath": "backend/.venv/bin/python", - "autoDocstring.customTemplatePath": ".vscode/extensions/autoDocstring/docstring.mustache", - "files.exclude": { - "**/__pycache__": true, - "**/.pytest_cache": true, - "**/.mypy_cache": true, - "**/.hypothesis": true - }, - "editor.tabSize": 2, - "editor.rulers": [ - 80 - ], - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" - }, - "workbench.colorCustomizations": { - "editorRuler.foreground": "#008000" - }, - "[md]": { - "editor.tabSize": 4 - }, - "[python]": { - "editor.tabSize": 4, - "editor.defaultFormatter": "ms-python.black-formatter", - }, - "cSpell.words": [ - "codeforlife", - "klass", - "ocado", - "kurono", - "pipenv" - ] - }, - "extensions": [ - "github.vscode-pull-request-github", - "redhat.vscode-yaml", - "davidanson.vscode-markdownlint", - "bierner.markdown-mermaid", - "ms-python.python", - "ms-python.pylint", - "ms-python.isort", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "ms-python.black-formatter", - "streetsidesoftware.code-spell-checker", - "tamasfe.even-better-toml", - "kevinrose.vsc-python-indent", - "batisteo.vscode-django", - "njpwerner.autodocstring", - "visualstudioexptteam.vscodeintellicode", - "wholroyd.jinja", - "qwtel.sqlite-viewer" - ] - } - } -} \ No newline at end of file diff --git a/.devcontainer/codeforlife-portal/devcontainer.json b/.devcontainer/codeforlife-portal/devcontainer.json deleted file mode 100644 index 524e0927..00000000 --- a/.devcontainer/codeforlife-portal/devcontainer.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "codeforlife-portal", - "dockerComposeFile": [ - "../../docker-compose.yml" - ], - "service": "base-service", - "shutdownAction": "none", - "workspaceFolder": "/workspace/codeforlife-portal", - "remoteUser": "root", - "postCreateCommand": "pipenv install --dev", - "features": { - "ghcr.io/devcontainers/features/python:1": { - "version": "3.8", - "installTools": false - }, - "ghcr.io/devcontainers-contrib/features/pipenv:2": { - "version": "2023.11.15" - } - }, - "customizations": { - "vscode": { - //TODO: Specify preferred settings and extensions once defined - "settings": { - "python.defaultInterpreterPath": ".venv/bin/python" - }, - "extensions": [ - "github.vscode-pull-request-github", - "redhat.vscode-yaml", - "davidanson.vscode-markdownlint", - "bierner.markdown-mermaid", - "ms-python.python", - "ms-python.pylint", - "ms-python.isort", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "ms-python.black-formatter", - "streetsidesoftware.code-spell-checker", - "tamasfe.even-better-toml", - "kevinrose.vsc-python-indent", - "batisteo.vscode-django", - "njpwerner.autodocstring", - "visualstudioexptteam.vscodeintellicode", - "wholroyd.jinja", - "qwtel.sqlite-viewer" - ] - } - } -} \ No newline at end of file diff --git a/.devcontainer/codeforlife-service-template/devcontainer.json b/.devcontainer/codeforlife-service-template/devcontainer.json deleted file mode 100644 index 799b5e2c..00000000 --- a/.devcontainer/codeforlife-service-template/devcontainer.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "codeforlife-service-template", - "dockerComposeFile": [ - "../../docker-compose.yml" - ], - "service": "base-service", - "shutdownAction": "none", - "workspaceFolder": "/workspace/codeforlife-service-template", - "remoteUser": "root", - "features": { - "ghcr.io/devcontainers/features/python:1": { - "version": "3.8", - "installTools": false - }, - "ghcr.io/devcontainers-contrib/features/pipenv:2": { - "version": "2023.11.15" - }, - "ghcr.io/devcontainers/features/node:1": { - "version": "18" - } - }, - "customizations": { - "vscode": { - //TODO: Specify preferred settings and extensions once defined - "settings": { - "python.defaultInterpreterPath": "backend/.venv/bin/python" - }, - "extensions": [ - "github.vscode-pull-request-github", - "redhat.vscode-yaml", - "davidanson.vscode-markdownlint", - "bierner.markdown-mermaid", - "ms-python.python", - "ms-python.pylint", - "ms-python.isort", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "ms-python.black-formatter", - "streetsidesoftware.code-spell-checker", - "tamasfe.even-better-toml", - "kevinrose.vsc-python-indent", - "batisteo.vscode-django", - "njpwerner.autodocstring", - "visualstudioexptteam.vscodeintellicode", - "wholroyd.jinja", - "qwtel.sqlite-viewer" - ] - } - } -} \ No newline at end of file diff --git a/.devcontainer/codeforlife-sso/devcontainer.json b/.devcontainer/codeforlife-sso/devcontainer.json deleted file mode 100644 index 5624575b..00000000 --- a/.devcontainer/codeforlife-sso/devcontainer.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "codeforlife-sso", - "dockerComposeFile": [ - "../../docker-compose.yml" - ], - "service": "base-service", - "shutdownAction": "none", - "workspaceFolder": "/workspace/codeforlife-sso", - "remoteUser": "root", - "features": { - "ghcr.io/devcontainers/features/python:1": { - "version": "3.8", - "installTools": false - }, - "ghcr.io/devcontainers-contrib/features/pipenv:2": { - "version": "2023.11.15" - } - }, - "customizations": { - "vscode": { - //TODO: Specify preferred settings and extensions once defined - "settings": { - "python.defaultInterpreterPath": "backend/.venv/bin/python" - }, - "extensions": [ - "github.vscode-pull-request-github", - "redhat.vscode-yaml", - "davidanson.vscode-markdownlint", - "bierner.markdown-mermaid", - "ms-python.python", - "ms-python.pylint", - "ms-python.isort", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "ms-python.black-formatter", - "streetsidesoftware.code-spell-checker", - "tamasfe.even-better-toml", - "kevinrose.vsc-python-indent", - "batisteo.vscode-django", - "njpwerner.autodocstring", - "visualstudioexptteam.vscodeintellicode", - "wholroyd.jinja", - "qwtel.sqlite-viewer" - ] - } - } -} \ No newline at end of file diff --git a/.devcontainer/rapid-router/devcontainer.json b/.devcontainer/rapid-router/devcontainer.json deleted file mode 100644 index e201eae0..00000000 --- a/.devcontainer/rapid-router/devcontainer.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "rapid-router", - "dockerComposeFile": [ - "../../docker-compose.yml" - ], - "service": "base-service", - "shutdownAction": "none", - "workspaceFolder": "/workspace/rapid-router", - "remoteUser": "root", - "postCreateCommand": "pipenv install --dev", - "features": { - "ghcr.io/devcontainers/features/python:1": { - "version": "3.8", - "installTools": false - }, - "ghcr.io/devcontainers-contrib/features/pipenv:2": { - "version": "2023.11.15" - } - }, - "customizations": { - "vscode": { - //TODO: Specify preferred settings and extensions once defined - "settings": { - "python.defaultInterpreterPath": ".venv/bin/python" - }, - "extensions": [ - "github.vscode-pull-request-github", - "redhat.vscode-yaml", - "davidanson.vscode-markdownlint", - "bierner.markdown-mermaid", - "ms-python.python", - "ms-python.pylint", - "ms-python.isort", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "ms-python.black-formatter", - "streetsidesoftware.code-spell-checker", - "tamasfe.even-better-toml", - "kevinrose.vsc-python-indent", - "batisteo.vscode-django", - "njpwerner.autodocstring", - "visualstudioexptteam.vscodeintellicode", - "wholroyd.jinja", - "qwtel.sqlite-viewer" - ] - } - } -} \ No newline at end of file diff --git a/.github/actions/git/setup-bot/action.yaml b/.github/actions/git/setup-bot/action.yaml index 2c45e3b4..5e9bdfcb 100644 --- a/.github/actions/git/setup-bot/action.yaml +++ b/.github/actions/git/setup-bot/action.yaml @@ -3,7 +3,7 @@ description: "Sets up CFL's bot as the Git user." runs: using: composite steps: - - name: ⚙️ Set up cfl-bot as Git user + - name: 🤖 Set up cfl-bot as Git user shell: bash run: | git config --local user.name cfl-bot diff --git a/.github/workflows/configure-submodules.yaml b/.github/workflows/configure-submodules.yaml new file mode 100644 index 00000000..45841e83 --- /dev/null +++ b/.github/workflows/configure-submodules.yaml @@ -0,0 +1,35 @@ +name: Configure Submodules + +on: + push: + branches: + - main + paths: + - '.submodules/**' + workflow_dispatch: + +jobs: + configure: + runs-on: ubuntu-latest + env: + PYTHON_VERSION: 3.11 + WORKING_DIR: .submodules + steps: + - name: 🛫 Checkout + uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: 🐍 Set up Python ${{ env.PYTHON_VERSION }} Environment + uses: ocadotechnology/codeforlife-workspace/.github/actions/python/setup-environment@main + with: + python-version: ${{ env.PYTHON_VERSION }} + working-directory: ${{ env.WORKING_DIR }} + + - uses: ocadotechnology/codeforlife-workspace/.github/actions/git/setup-bot@main + + - name: ⚙️ Configure Submodules + working-directory: ${{ env.WORKING_DIR }} + run: pipenv run python . + env: + GIT_PUSH_CHANGES: '0' # TODO: set to 1 and allow bot to force push diff --git a/.submodules/.venv/.gitkeep b/.submodules/.venv/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.submodules/Pipfile b/.submodules/Pipfile new file mode 100644 index 00000000..c93ff348 --- /dev/null +++ b/.submodules/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] +black = "==23.1.0" +pytest = "==7.2.1" +mypy = "==1.6.1" +pylint = "==3.0.2" + +[requires] +python_version = "3.11" diff --git a/.submodules/Pipfile.lock b/.submodules/Pipfile.lock new file mode 100644 index 00000000..50fe2e58 --- /dev/null +++ b/.submodules/Pipfile.lock @@ -0,0 +1,217 @@ +{ + "_meta": { + "hash": { + "sha256": "be5bb9491279fa95591b4214435181afcd323d9ec48411dd6a25ccc240ee79c7" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": {}, + "develop": { + "astroid": { + "hashes": [ + "sha256:4148645659b08b70d72460ed1921158027a9e53ae8b7234149b1400eddacbb93", + "sha256:92fcf218b89f449cdf9f7b39a269f8d5d617b27be68434912e11e79203963a17" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.3" + }, + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "black": { + "hashes": [ + "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd", + "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555", + "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481", + "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468", + "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9", + "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a", + "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958", + "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580", + "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26", + "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32", + "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8", + "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753", + "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b", + "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074", + "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651", + "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24", + "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6", + "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad", + "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac", + "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221", + "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06", + "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27", + "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648", + "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739", + "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "dill": { + "hashes": [ + "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca", + "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7" + ], + "markers": "python_version >= '3.11'", + "version": "==0.3.8" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "isort": { + "hashes": [ + "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", + "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.13.2" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy": { + "hashes": [ + "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7", + "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e", + "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c", + "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169", + "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208", + "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0", + "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1", + "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1", + "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7", + "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45", + "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143", + "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5", + "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f", + "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd", + "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245", + "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f", + "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332", + "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30", + "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183", + "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f", + "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85", + "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46", + "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71", + "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660", + "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb", + "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c", + "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.6.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", + "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + ], + "markers": "python_version >= '3.8'", + "version": "==4.2.0" + }, + "pluggy": { + "hashes": [ + "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.0" + }, + "pylint": { + "hashes": [ + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, + "pytest": { + "hashes": [ + "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5", + "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.2.1" + }, + "tomlkit": { + "hashes": [ + "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b", + "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.4" + }, + "typing-extensions": { + "hashes": [ + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + ], + "markers": "python_version >= '3.8'", + "version": "==4.10.0" + } + } +} diff --git a/.submodules/__main__.py b/.submodules/__main__.py new file mode 100644 index 00000000..6e79dde6 --- /dev/null +++ b/.submodules/__main__.py @@ -0,0 +1,169 @@ +""" +© Ocado Group +Created on 02/03/2024 at 23:46:28(+00:00). + +This file is used to configure CFL's submodules using the global-config defined +in the config.jsonc file in this directory. + +By default, the global-config is *merged* into any existing config within each +submodule. That is, any values defined in the global-config will override the +values found in a submodule's config but if a submodule has key:value pairs not +present in the global-config, they will remain. However, in some cases, the +behavior is to override the values (values not present in the global-config will +be removed). +""" + +import os +import subprocess +from pathlib import Path + +from configs import GlobalSubmoduleConfig, JsonDict, JsonValue, load_global_configs +from helpers import ( + DOT_SUBMODULES_DIR, + GIT_PUSH_CHANGES, + git_commit_and_push, + merge_json_dicts, + merge_json_lists_of_json_objects, + merge_submodule_file, +) + + +def _merge_vscode_code_snippets(global_code_snippets: JsonDict): + def merge( + submodule_code_snippets: JsonValue, + global_code_snippets: JsonDict, + ): + assert isinstance(submodule_code_snippets, dict) + + for key, code_snippet in global_code_snippets.items(): + submodule_code_snippets[key] = code_snippet + + return submodule_code_snippets + + merge_submodule_file( + ".vscode/codeforlife.code-snippets", + global_code_snippets, + merge, + ) + + +def merge_global_config(global_config: GlobalSubmoduleConfig): + if global_config.devcontainer: + merge_submodule_file( + ".devcontainer.json", + global_config.devcontainer, + merge_json_dicts, + ) + + if global_config.vscode: + # Create .vscode directory if not exists. + Path(".vscode").mkdir(exist_ok=True) + + if global_config.vscode.settings: + merge_submodule_file( + ".vscode/settings.json", + global_config.vscode.settings, + merge_json_dicts, + ) + + if global_config.vscode.tasks: + merge_submodule_file( + ".vscode/tasks.json", + global_config.vscode.tasks, + merge=lambda submodule_tasks, global_tasks: ( + merge_json_lists_of_json_objects( + submodule_tasks, + global_tasks, + list_names_and_obj_id_fields=[("tasks", "label")], + ) + ), + ) + + if global_config.vscode.launch: + merge_submodule_file( + ".vscode/launch.json", + global_config.vscode.launch, + merge=lambda submodule_launch, global_launch: ( + merge_json_lists_of_json_objects( + submodule_launch, + global_launch, + list_names_and_obj_id_fields=[("configurations", "name")], + ) + ), + ) + + if global_config.vscode.codeSnippets: + _merge_vscode_code_snippets(global_config.vscode.codeSnippets) + + if global_config.workspace: + merge_submodule_file( + "codeforlife.code-workspace", + global_config.workspace, + merge=lambda submodule_workspace, global_workspace: ( + merge_json_lists_of_json_objects( + submodule_workspace, + global_workspace, + list_names_and_obj_id_fields=[("folders", "path")], + ) + ), + ) + + +def main() -> None: + global_configs, inheritances = load_global_configs() + + # Process each config. + for key, global_config in global_configs.items(): + # Skip config if it's not to be merged into any submodules. + if not global_config.submodules: + continue + + # Print config details. + print(f"Key: {key}") + if global_config.description: + print(f"Description: {global_config.description}") + + # Print config inheritances. + if inheritances[key]: + print("Inherits:") + for inheritance in inheritances[key]: + inheritance_description = global_configs[inheritance].description + print( + f" - {inheritance}" + + ( + f": {inheritance_description}" + if inheritance_description + else "" + ) + ) + + # Print config submodules. + print("Submodules:") + for submodule in global_config.submodules: + print(f" - {submodule}") + + # Merge inherited configs and config into submodule in order. + for submodule in global_config.submodules: + # Change directory to submodule's directory. + os.chdir(f"{DOT_SUBMODULES_DIR}/../{submodule}") + + for inheritance in inheritances[key]: + merge_global_config(global_configs[inheritance]) + + merge_global_config(global_config) + + if GIT_PUSH_CHANGES: + git_commit_and_push(message="Configured submodule [skip ci]") + os.chdir(f"{DOT_SUBMODULES_DIR}/..") + subprocess.run(["git", "add", submodule], check=True) + + print("---") + + if GIT_PUSH_CHANGES: + git_commit_and_push(message="Configured submodules [skip ci]") + + print("Success!") + + +if __name__ == "__main__": + main() diff --git a/.submodules/configs.jsonc b/.submodules/configs.jsonc new file mode 100644 index 00000000..7ddec718 --- /dev/null +++ b/.submodules/configs.jsonc @@ -0,0 +1,452 @@ +// This file is used to define global submodule-configs for the submodules +// within CFL's workspace. This saves us having to manually repeat common +// configurations across 2+ submodules and avoids human error when accidentally +// copy/pasting something incorrectly. +// +// WARNING: This file is written in JSONC format (JSON with comments). However, +// only single lines comments with no preceding text are allowed. For example: +// - "// single-line comment, no preceding text" ✅ +// - "{"age": 27} // single-line comment, some preceding text" 🚫 +// - "/* multi-line comment, no preceding text */" 🚫 +// - "{"age": 27} /* multi-line comment, some preceding text */" 🚫 +// +// These configs are processed by .submodules/__main__.py. To understand more +// about the shape of this data, please read .submodules/configs.py. +{ + "base": { + "description": "Base configuration to be inherited by all other configurations.", + "devcontainer": { + "dockerComposeFile": [ + "../docker-compose.yml" + ], + "service": "base-service", + "shutdownAction": "none", + "remoteUser": "root", + "customizations": { + "vscode": { + "extensions": [ + "visualstudioexptteam.vscodeintellicode", + "github.vscode-pull-request-github", + "redhat.vscode-yaml", + "davidanson.vscode-markdownlint", + "bierner.markdown-mermaid", + "streetsidesoftware.code-spell-checker" + ] + } + } + }, + "vscode": { + "settings": { + "editor.tabSize": 2, + "editor.rulers": [ + 80 + ], + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "[md]": { + "editor.tabSize": 4 + }, + "cSpell.words": [ + "codeforlife", + "klass", + "ocado", + "kurono", + "pipenv" + ] + } + }, + "workspace": { + "folders": [ + { + "path": "." + } + ], + "settings": { + "workbench.colorCustomizations": { + "editorRuler.foreground": "#008000" + } + } + } + }, + "javascript": { + "inherits": [ + "base" + ], + "description": "A devcontainer with a javascript environment.", + "submodules": [ + "codeforlife-package-javascript" + ], + "devcontainer": { + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "18" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint" + // "dsznajder.es7-react-js-snippets", + // "ecmel.vscode-html-css", + // "jock.svg" + ] + } + } + }, + "vscode": { + "settings": { + "typescript.preferences.quoteStyle": "single" + }, + "codeSnippets": { + "javascript.module.doccomment": { + "prefix": [ + "/" + ], + "scope": "javascript,typescript,javascriptreact,typescriptreact", + "body": [ + "/**", + " * © Ocado Group", + " * Created on $CURRENT_DATE/$CURRENT_MONTH/$CURRENT_YEAR at $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND($CURRENT_TIMEZONE_OFFSET).", + " *", + " * ${1:__description__}", + " */" + ] + } + } + } + }, + "python": { + "inherits": [ + "base" + ], + "description": "A devcontainer with a python environment.", + "submodules": [ + "codeforlife-deploy-appengine", + "codeforlife-package-python", + "codeforlife-portal", + "rapid-router" + ], + "devcontainer": { + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.8", + "installTools": false + }, + "ghcr.io/devcontainers-contrib/features/pipenv:2": { + "version": "2023.11.15" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "ms-python.debugpy", + "ms-python.pylint", + "ms-python.isort", + "ms-python.vscode-pylance", + "ms-python.mypy-type-checker", + "ms-python.black-formatter", + "qwtel.sqlite-viewer", + "njpwerner.autodocstring" + ] + } + } + }, + "vscode": { + "settings": { + "python.defaultInterpreterPath": ".venv/bin/python", + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "[python]": { + "editor.tabSize": 4, + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "files.exclude": { + "**/__pycache__": true, + "**/.pytest_cache": true, + "**/.mypy_cache": true, + "**/.hypothesis": true + }, + "isort.path": [ + ".venv/bin/python", + "-m", + "isort" + ], + "isort.args": [ + "--settings-file=pyproject.toml" + ], + "black-formatter.path": [ + ".venv/bin/python", + "-m", + "black" + ], + "black-formatter.args": [ + "--config", + "pyproject.toml" + ], + "mypy-type-checker.path": [ + ".venv/bin/python", + "-m", + "mypy" + ], + "mypy-type-checker.args": [ + "--config-file=pyproject.toml" + ], + "pylint.path": [ + ".venv/bin/python", + "-m", + "pylint" + ], + "pylint.args": [ + "--rcfile=pyproject.toml" + ], + "python.testing.pytestArgs": [ + "-n=auto", + "-c=pyproject.toml", + "." + ] + }, + "launch": { + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": false + }, + { + "name": "Pytest", + "type": "debugpy", + "request": "test", + "justMyCode": false, + "presentation": { + "hidden": true + } + } + ] + }, + "codeSnippets": { + "python.module.docstring": { + "prefix": [ + "\"\"\"", + "'''" + ], + "scope": "python", + "body": [ + "\"\"\"", + "© Ocado Group", + "Created on $CURRENT_DATE/$CURRENT_MONTH/$CURRENT_YEAR at $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND($CURRENT_TIMEZONE_OFFSET).", + "", + "${1:__description__}", + "\"\"\"" + ] + }, + "python.pylint.disable-next": { + "prefix": [ + "# pylint" + ], + "scope": "python", + "body": [ + "# pylint: disable-next=${1:__code_name__}" + ] + }, + "python.mypy.ignore": { + "prefix": [ + "# type" + ], + "scope": "python", + "body": [ + "# type: ignore[${1:__code_name__}]" + ] + } + } + }, + "workspace": { + "folders": [], + "settings": { + "autoDocstring.customTemplatePath": ".vscode/extensions/autoDocstring/docstring.mustache" + } + } + }, + "service": { + "inherits": [ + "javascript", + "python" + ], + "description": "A devcontainer for a micro-service.", + "submodules": [ + "codeforlife-service-template", + "codeforlife-portal-react", + "codeforlife-sso" + ], + "devcontainer": { + "postCreateCommand": "./setup", + "mounts": [ + "source=./codeforlife-package-javascript,target=/workspace/codeforlife-package-javascript,type=bind,consistency=cached", + "source=./codeforlife-package-python,target=/workspace/codeforlife-package-python,type=bind,consistency=cached" + ] + }, + "vscode": { + "settings": { + "python.defaultInterpreterPath": "backend/.venv/bin/python", + "python.analysis.extraPaths": [ + "../codeforlife-package-python" + ], + "!isort.path": [ + "backend/.venv/bin/python", + "-m", + "isort" + ], + "!isort.args": [ + "--settings-file=backend/pyproject.toml" + ], + "black-formatter.cwd": "${workspaceFolder}/backend", + "mypy-type-checker.cwd": "${workspaceFolder}/backend", + "pylint.cwd": "${workspaceFolder}/backend", + "python.testing.cwd": "${workspaceFolder}/backend" + }, + "tasks": { + "version": "2.0.0", + "tasks": [ + { + "label": "start-react-dev-server", + "isBackground": true, + "type": "npm", + "script": "start", + "options": { + "env": { + "BROWSER": "none" + } + }, + "path": "frontend", + "problemMatcher": [] + }, + { + "label": "pipenv-install-dev", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}/backend" + }, + "command": "pipenv install --dev" + }, + { + "label": "migrate-db", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}/backend" + }, + "dependsOn": [ + "pipenv-install-dev" + ], + "command": "pipenv run python ./manage.py migrate" + } + ] + }, + "launch": { + "version": "0.2.0", + "configurations": [ + { + "name": "React Dev Server", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000", + "preLaunchTask": "start-react-dev-server" + }, + { + "name": "Django Server", + "type": "debugpy", + "request": "launch", + "django": true, + "justMyCode": false, + "program": "${workspaceFolder}/backend/manage.py", + "?args": [ + "runserver", + "localhost:8000" + ], + "preLaunchTask": "migrate-db" + } + ] + } + }, + "workspace": { + "folders": [ + { + "path": "../codeforlife-package-python", + "name": "package-python" + }, + { + "path": "../codeforlife-package-javascript", + "name": "package-javascript" + } + ] + } + }, + "service+sso": { + "inherits": [ + "service" + ], + "description": "A service that also runs the SSO service in the background.", + // TODO: set submodules after testing how this would work. + "submodules": [], + "devcontainer": { + "mounts": [ + "source=./codeforlife-sso,target=/workspace/codeforlife-sso,type=bind,consistency=cached" + ] + }, + "vscode": { + "tasks": { + "version": "2.0.0", + "tasks": [ + { + "label": "run-sso-server", + "type": "shell", + "isBackground": true, + "options": { + "cwd": "${workspaceFolder}/../codeforlife-sso/backend", + "env": { + "DB_NAME": "${fileWorkspaceFolder}/backend/db.sqlite3", + "SERVICE_NAME": "sso", + "SERVICE_PORT": "8001" + } + }, + "dependsOn": [ + "migrate-db" + ], + "command": "pipenv run python ./manage.py runserver localhost:8001" + } + ] + }, + "launch": { + "version": "0.2.0", + "configurations": [ + { + "name": "Django Server", + "type": "python", + "request": "launch", + "django": true, + "justMyCode": false, + "program": "${fileWorkspaceFolder}/backend/manage.py", + "args": [ + "runserver", + "localhost:8000" + ], + "preLaunchTask": "run-sso-server" + } + ] + } + }, + "workspace": { + "folders": [ + { + "path": "../codeforlife-sso", + "name": "sso" + } + ] + } + } +} \ No newline at end of file diff --git a/.submodules/configs.py b/.submodules/configs.py new file mode 100644 index 00000000..a512090a --- /dev/null +++ b/.submodules/configs.py @@ -0,0 +1,136 @@ +""" +© Ocado Group +Created on 06/03/2024 at 10:58:17(+00:00). + +Loads the global submodule-configs found in .submodules/configs.jsonc. +""" + +import os +import typing as t +from collections import Counter +from dataclasses import dataclass + +from helpers import DOT_SUBMODULES_DIR, load_jsonc + +# JSON type hints. +JsonList = t.List["JsonValue"] +JsonDict = t.Dict[str, "JsonValue"] +JsonValue = t.Union[None, int, str, bool, JsonList, JsonDict] + +AnyJsonValue = t.TypeVar("AnyJsonValue", bound=JsonValue) + + +@dataclass(frozen=True) +class VSCode: + """JSON files contained within the .vscode directory.""" + + # The config for settings.json. + settings: t.Optional[JsonDict] = None + # The config for tasks.json. + tasks: t.Optional[JsonDict] = None + # The config for launch.json. + launch: t.Optional[JsonDict] = None + # The config for codeforlife.code-snippets. + codeSnippets: t.Optional[JsonDict] = None + + +@dataclass(frozen=True) +class GlobalSubmoduleConfig: + """ + A global configuration to be applied to a number of submodules or inherited + by other global configurations. + """ + + # The configs this config inherits. + inherits: t.Optional[t.List[str]] = None + # The submodules this config should be merged into. + submodules: t.Optional[t.List[str]] = None + # A description of this config's target. + description: t.Optional[str] = None + # The VSCode config files to merge with. + vscode: t.Optional[VSCode] = None + # The devcontainer config. + devcontainer: t.Optional[JsonDict] = None + # The workspace config. + workspace: t.Optional[JsonDict] = None + + +GlobalConfigDict = t.Dict[str, GlobalSubmoduleConfig] +InheritanceDict = t.Dict[str, t.Tuple[str, ...]] + + +def load_global_configs() -> t.Tuple[GlobalConfigDict, InheritanceDict]: + # Change directory to .submodules. + os.chdir(DOT_SUBMODULES_DIR) + + # Load the configs file. + with open("configs.jsonc", "r", encoding="utf-8") as configs_file: + global_json_configs = load_jsonc(configs_file) + + assert isinstance(global_json_configs, dict) + + # Convert the JSON objects to Python objects. + global_configs: GlobalConfigDict = {} + for key, global_json_config in global_json_configs.items(): + assert isinstance(global_json_config, dict) + + json_vscode = global_json_config.pop("vscode", None) + if json_vscode is None: + vscode = None + else: + assert isinstance(json_vscode, dict) + vscode = VSCode(**json_vscode) # type: ignore[arg-type] + + global_configs[key] = GlobalSubmoduleConfig( + vscode=vscode, + **global_json_config, # type: ignore[arg-type] + ) + + # Assert each submodule is specified only once. + for submodule, count in Counter( + [ + submodule + for global_config in global_configs.values() + for submodule in (global_config.submodules or []) + ] + ).items(): + assert count == 1, f"Submodule: {submodule} specified more than once." + + inheritances: InheritanceDict = {} + for key, global_config in global_configs.items(): + inheritances[key] = _get_inheritances(global_config, global_configs) + + return global_configs, inheritances + + +def _get_inheritances( + global_config: GlobalSubmoduleConfig, + global_configs: GlobalConfigDict, +): + + def get_inheritances( + config: GlobalSubmoduleConfig, inheritances: t.List[str], index: int + ): + if not config.inherits: + return + + config_inheritances = [] + for inheritance in config.inherits: + if inheritance not in inheritances: + config_inheritances.append(inheritance) + + for inheritance in config_inheritances[::-1]: + inheritances.insert(index, inheritance) + + for inheritance in config_inheritances: + get_inheritances( + global_configs[inheritance], + inheritances, + inheritances.index(inheritance), + ) + + inheritances: t.List[str] = [] + + get_inheritances(global_config, inheritances, index=0) + + return tuple(inheritances) diff --git a/.submodules/helpers.py b/.submodules/helpers.py new file mode 100644 index 00000000..badd456a --- /dev/null +++ b/.submodules/helpers.py @@ -0,0 +1,162 @@ +""" +© Ocado Group +Created on 06/03/2024 at 11:05:06(+00:00). + +General helpers. +""" + +import json +import os +import re +import subprocess +import typing as t +from io import TextIOWrapper + +if t.TYPE_CHECKING: + from configs import AnyJsonValue, JsonDict, JsonList, JsonValue + +# Path to the .submodules directory. +DOT_SUBMODULES_DIR = os.path.dirname(os.path.realpath(__file__)) +# Whether or not to git-push the changes. +GIT_PUSH_CHANGES = bool(int(os.getenv("GIT_PUSH_CHANGES", "0"))) + + +def load_jsonc(file: TextIOWrapper) -> "JsonValue": + file.seek(0) + raw_json_with_comments = file.read() + if not raw_json_with_comments: + return None + + # Remove single-line comments that are only preceded by white spaces. + raw_json_without_comments = re.sub( + r"^ *\/\/.*", "", raw_json_with_comments, flags=re.MULTILINE + ) + + return json.loads(raw_json_without_comments) + + +def git_commit_and_push(message: str): + git_diff = subprocess.run( + ["git", "diff", "--cached"], check=True, stdout=subprocess.PIPE + ).stdout.decode("utf-8") + + if git_diff: + subprocess.run(["git", "commit", "-m", f'"{message}"'], check=True) + subprocess.run(["git", "push"], check=True) + + +def merge_json_lists(submodule_value: "JsonValue", global_list: "JsonList"): + if not isinstance(submodule_value, list): + return global_list + + json_list = submodule_value.copy() + + for value in global_list: + if isinstance(value, (int, str)): + if value not in json_list: + json_list.append(value) + else: + raise NotImplementedError( + f"Haven't implemented support for values of type {type(value)}." + ) + + return json_list + + +def merge_json_dicts(submodule_value: "JsonValue", global_dict: "JsonDict"): + if not isinstance(submodule_value, dict): + return global_dict + + json_dict = submodule_value.copy() + + for key, value in global_dict.items(): + override_submodule_value = key.startswith("!") + keep_submodule_value = key.startswith("?") + if override_submodule_value or keep_submodule_value: + key = key[1:] + + if key not in json_dict: + json_dict[key] = value + elif keep_submodule_value: + continue + + if value is None or isinstance(value, (str, int, bool)): + json_dict[key] = value + elif isinstance(value, dict): + json_dict[key] = ( + value.copy() + if override_submodule_value + else merge_json_dicts(json_dict[key], value) + ) + elif isinstance(value, list): + json_dict[key] = ( + value.copy() + if override_submodule_value + else merge_json_lists(json_dict[key], value) + ) + + return json_dict + + +def merge_json_lists_of_json_objects( + submodule_value: "JsonValue", + global_dict: "JsonDict", + list_names_and_obj_id_fields: t.Iterable[t.Tuple[str, str]], +): + global_dict = global_dict.copy() + + if not isinstance(submodule_value, dict): + return global_dict + + obj_lists: t.Dict[str, t.Tuple[JsonList, JsonList]] = {} + for list_name, _ in list_names_and_obj_id_fields: + current_list = submodule_value.pop(list_name) + assert isinstance(current_list, list) + latest_list = global_dict.pop(list_name) + assert isinstance(latest_list, list) + + obj_lists[list_name] = (current_list, latest_list) + + merged_dict = merge_json_dicts(submodule_value, global_dict) + + for list_name, obj_id_field in list_names_and_obj_id_fields: + current_list, latest_list = obj_lists[list_name] + + merged_list = current_list.copy() + for obj in latest_list: + assert isinstance(obj, dict) + + for current_obj in current_list.copy(): + assert isinstance(current_obj, dict) + + if obj[obj_id_field] == current_obj[obj_id_field]: + current_list.remove(current_obj) + merged_list.remove(current_obj) + + obj = merge_json_dicts(current_obj, obj) + break + + merged_list.append(obj) + + merged_dict[list_name] = merged_list + + return merged_dict + + +def merge_submodule_file( + file: str, + global_value: "AnyJsonValue", + merge: t.Callable[["JsonValue", "AnyJsonValue"], "JsonValue"], +): + with open(file, "a+", encoding="utf-8") as submodule_file: + submodule_value = load_jsonc(submodule_file) + if submodule_file is None: + value = global_value + else: + value = merge(submodule_value, global_value) + submodule_file.truncate(0) + + json.dump(value, submodule_file, indent=2, sort_keys=True) + + if GIT_PUSH_CHANGES: + subprocess.run(["git", "add", file], check=True) diff --git a/.vscode/codeforlife.code-snippets b/.vscode/codeforlife.code-snippets new file mode 100644 index 00000000..c54a2b16 --- /dev/null +++ b/.vscode/codeforlife.code-snippets @@ -0,0 +1,35 @@ +{ + "python.module.docstring": { + "body": [ + "\"\"\"", + "\u00a9 Ocado Group", + "Created on $CURRENT_DATE/$CURRENT_MONTH/$CURRENT_YEAR at $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND($CURRENT_TIMEZONE_OFFSET).", + "", + "${1:__description__}", + "\"\"\"" + ], + "prefix": [ + "\"\"\"", + "'''" + ], + "scope": "python" + }, + "python.mypy.ignore": { + "body": [ + "# type: ignore[${1:__code_name__}]" + ], + "prefix": [ + "# type" + ], + "scope": "python" + }, + "python.pylint.disable-next": { + "body": [ + "# pylint: disable-next=${1:__code_name__}" + ], + "prefix": [ + "# pylint" + ], + "scope": "python" + } +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..014d2ef0 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "console": "integratedTerminal", + "justMyCode": false, + "name": "Python: Current File", + "program": "${file}", + "request": "launch", + "type": "debugpy" + }, + { + "justMyCode": false, + "name": "Pytest", + "presentation": { + "hidden": true + }, + "request": "test", + "type": "debugpy" + } + ], + "version": "0.2.0" +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..22fa2483 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,32 @@ +{ + "[md]": { + "editor.tabSize": 4 + }, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.tabSize": 4 + }, + "cSpell.words": [ + "codeforlife", + "klass", + "ocado", + "kurono", + "pipenv" + ], + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "editor.formatOnSave": true, + "editor.rulers": [ + 80 + ], + "editor.tabSize": 2, + "files.exclude": { + "**/.hypothesis": true, + "**/.mypy_cache": true, + "**/.pytest_cache": true, + "**/__pycache__": true + }, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false +} \ No newline at end of file diff --git a/.vscode/workspace.code-snippets b/.vscode/workspace.code-snippets deleted file mode 100644 index e2db498c..00000000 --- a/.vscode/workspace.code-snippets +++ /dev/null @@ -1,51 +0,0 @@ -{ - "module.docstring": { - "prefix": [ - "module.docstring", - "\"\"\"", - "'''" - ], - "scope": "python", - "body": [ - "\"\"\"", - "© Ocado Group", - "Created on $CURRENT_DATE/$CURRENT_MONTH/$CURRENT_YEAR at $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND($CURRENT_TIMEZONE_OFFSET)." - "", - "${1:__description__}", - "\"\"\"" - ] - }, - "module.doccomment": { - "prefix": [ - "module.doccomment", - "/" - ], - "scope": "javascript,typescript,javascriptreact,typescriptreact", - "body": [ - "/**", - " * © Ocado Group", - " * Created on $CURRENT_DATE/$CURRENT_MONTH/$CURRENT_YEAR at $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND($CURRENT_TIMEZONE_OFFSET)." - " *", - " * ${1:__description__}", - " */" - ] - }, - "pylint.disable-next": { - "prefix": [ - "# pylint" - ], - "scope": "python", - "body": [ - "# pylint: disable-next=${1:__code_name__}" - ] - }, - "mypy.ignore": { - "prefix": [ - "# type" - ], - "scope": "python", - "body": [ - "# type: ignore[${1:__code_name__}]" - ] - } -} \ No newline at end of file diff --git a/codeforlife-deploy-appengine b/codeforlife-deploy-appengine index b8b84e18..4b84aedd 160000 --- a/codeforlife-deploy-appengine +++ b/codeforlife-deploy-appengine @@ -1 +1 @@ -Subproject commit b8b84e184b796f21101b2523d8d07ea12743f1f1 +Subproject commit 4b84aedd671c22ac14607f3a2922740680e6c5ea diff --git a/codeforlife-package-javascript b/codeforlife-package-javascript index a15b89df..edad35f2 160000 --- a/codeforlife-package-javascript +++ b/codeforlife-package-javascript @@ -1 +1 @@ -Subproject commit a15b89dfa582409faa290b47f214112eef7b4497 +Subproject commit edad35f2de1e8cf07f383c0437af6c5eabd81733 diff --git a/codeforlife-package-python b/codeforlife-package-python index 6cd1c6ac..f51788e5 160000 --- a/codeforlife-package-python +++ b/codeforlife-package-python @@ -1 +1 @@ -Subproject commit 6cd1c6ac3cd239a5d500fa47a800c797648e55dc +Subproject commit f51788e57bd9f5badb65cf0446a396f8327349e5 diff --git a/codeforlife-portal b/codeforlife-portal index 410e3480..2d6a6130 160000 --- a/codeforlife-portal +++ b/codeforlife-portal @@ -1 +1 @@ -Subproject commit 410e3480486d719e871e24f1fe8c46bec1651dcb +Subproject commit 2d6a61303231d7a7425ca4bdab86e0cd346690bf diff --git a/codeforlife-portal-react b/codeforlife-portal-react index b8255b29..0c42b56b 160000 --- a/codeforlife-portal-react +++ b/codeforlife-portal-react @@ -1 +1 @@ -Subproject commit b8255b29f46eca0900eabd088a748d8e781b3ddb +Subproject commit 0c42b56bd5d37fc9dd3071d48f9671c4be9a9fcd diff --git a/codeforlife-service-template b/codeforlife-service-template index 753394ed..a08d96e1 160000 --- a/codeforlife-service-template +++ b/codeforlife-service-template @@ -1 +1 @@ -Subproject commit 753394ed9fe62cd73d3258e3602bfba949df1666 +Subproject commit a08d96e1b882b70b45881fc65c2b63e784265382 diff --git a/codeforlife-sso b/codeforlife-sso index e53ce14e..2a08ad73 160000 --- a/codeforlife-sso +++ b/codeforlife-sso @@ -1 +1 @@ -Subproject commit e53ce14e430bbb4bcef2b088095fb728507368df +Subproject commit 2a08ad7388afa3cb174968d9abae9ab286c459cd diff --git a/codeforlife.code-workspace b/codeforlife.code-workspace deleted file mode 100644 index de816a5f..00000000 --- a/codeforlife.code-workspace +++ /dev/null @@ -1,179 +0,0 @@ -{ - "folders": [ - { - "name": "workspace", - "path": "." - }, - { - "name": "portal", - "path": "codeforlife-portal" - }, - { - "name": "portal-react", - "path": "codeforlife-portal-react" - }, - { - "name": "kurono-badges", - "path": "codeforlife-kurono-badges" - }, - { - "name": "aimmo", - "path": "aimmo" - }, - { - "name": "rapid-router", - "path": "rapid-router" - }, - { - "name": "appengine", - "path": "codeforlife-deploy-appengine" - }, - { - "name": "package-python", - "path": "codeforlife-package-python" - }, - { - "name": "package-javascript", - "path": "codeforlife-package-javascript" - }, - { - "name": "service-template", - "path": "codeforlife-service-template" - }, - { - "name": "sso", - "path": "codeforlife-sso" - } - ], - "settings": { - "python.terminal.activateEnvironment": false, - "autoDocstring.customTemplatePath": ".vscode/extensions/autoDocstring/docstring.mustache", - "files.exclude": { - "**/__pycache__": true, - "**/.pytest_cache": true, - "**/.mypy_cache": true, - "**/.hypothesis": true - }, - "editor.tabSize": 2, - "editor.rulers": [ - 80 - ], - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": true - }, - "workbench.colorCustomizations": { - "editorRuler.foreground": "#008000" - }, - "[md]": { - "editor.tabSize": 4 - }, - "[python]": { - "editor.tabSize": 4, - "editor.defaultFormatter": "ms-python.black-formatter", - }, - "cSpell.words": [ - "codeforlife", - "klass", - "ocado", - "kurono", - "pipenv" - ], - }, - "extensions": { - "recommendations": [ - "dbaeumer.vscode-eslint", - "dsznajder.es7-react-js-snippets", - "tamasfe.even-better-toml", - "github.vscode-pull-request-github", - "ecmel.vscode-html-css", - "streetsidesoftware.code-spell-checker", - "bierner.markdown-mermaid", - "redhat.vscode-yaml", - "davidanson.vscode-markdownlint", - "jock.svg", - "donjayamanne.python-extension-pack", - "ms-python.python", - "ms-python.pylint", - "ms-python.isort", - "ms-python.vscode-pylance", - "ms-python.mypy-type-checker", - "ms-python.black-formatter", - "ms-toolsai.jupyter", - "ms-toolsai.vscode-jupyter-cell-tags", - "ms-toolsai.jupyter-keymap", - "ms-toolsai.jupyter-renderers", - "ms-toolsai.vscode-jupyter-slideshow", - "ms-kubernetes-tools.vscode-kubernetes-tools", - "ms-azuretools.vscode-docker", - "qwtel.sqlite-viewer" - ] - }, - "launch": { - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal", - "justMyCode": true - }, - { - "name": "Django Server (+SSO)", - "type": "python", - "request": "launch", - "django": true, - "justMyCode": false, - "program": "${fileWorkspaceFolder}/backend/manage.py", - "args": [ - "runserver", - "localhost:8000" - ], - "preLaunchTask": "run-sso-server" - } - ] - }, - "tasks": { - "version": "2.0.0", - "tasks": [ - { - "label": "install-dev-deps", - "type": "shell", - "options": { - "cwd": "${fileWorkspaceFolder}/backend" - }, - "command": "pipenv install --dev" - }, - { - "label": "migrate-db", - "type": "shell", - "options": { - "cwd": "${fileWorkspaceFolder}/backend" - }, - "dependsOn": [ - "install-dev-deps" - ], - "command": "pipenv run python ./manage.py migrate" - }, - { - "label": "run-sso-server", - "type": "shell", - "isBackground": true, - "options": { - "cwd": "${workspaceFolder}/codeforlife-sso/backend", - "env": { - "DB_NAME": "${fileWorkspaceFolder}/backend/db.sqlite3", - "SERVICE_NAME": "sso", - "SERVICE_PORT": "8001" - } - }, - "dependsOn": [ - "migrate-db" - ], - "command": "pipenv run python ./manage.py runserver localhost:8001" - } - ] - } -} \ No newline at end of file diff --git a/rapid-router b/rapid-router index 3689e251..3749c61d 160000 --- a/rapid-router +++ b/rapid-router @@ -1 +1 @@ -Subproject commit 3689e251694da440c3f9cffe713b0844a108deb7 +Subproject commit 3749c61d5f534cfffbd9d56e3b79a62bffa540ce