From 54e7f1dec874b3dc90785da5a71ac874afaa4928 Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Wed, 8 Apr 2026 21:50:25 +0200 Subject: [PATCH 01/18] Fix doc and MySQL docker file --- docker/mysql/Dockerfile | 2 +- docs/SETUP.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/mysql/Dockerfile b/docker/mysql/Dockerfile index 8973a66..8d547d1 100644 --- a/docker/mysql/Dockerfile +++ b/docker/mysql/Dockerfile @@ -1,3 +1,3 @@ -FROM mysql:8.0-debian +FROM ubuntu/mysql:8.0-20.04_beta RUN apt-get update && apt-get -q -y install nano make diff --git a/docs/SETUP.md b/docs/SETUP.md index ed5ec93..280c860 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -6,7 +6,7 @@ In the root folder, copy the file _configuration.json.dist_ to _configuration.js Next, just do a __make start__, and then run __make test__ to run the tests and import the local DB with test features. You're good to go! -Ther test DB it just imported has a user. You can login with the following credentials : +The test DB which was just imported has already a given user. You can login with the following credentials : * username: _mephistophelesz_ * password: _barz_ From c40b9edd7168deff7f0145181497453ef88c627f Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Wed, 8 Apr 2026 22:06:56 +0200 Subject: [PATCH 02/18] Migration to Poetry, bumped flask and python --- .circleci/config.yml | 10 +- .gitignore | 1 + Makefile | 6 +- app.py | 2 +- docker-compose.yml | 4 +- docker/python/Dockerfile | 15 +- docker/python/Pipfile | 17 - docker/python/Pipfile.lock | 308 --------------- docker/python/requirements.txt | 32 -- poetry.lock | 605 +++++++++++++++++++++++++++++ pyproject.toml | 36 ++ src/repository/story_repository.py | 1 - src/service/abstract_service.py | 2 +- src/service/transaction_service.py | 8 +- standard.rc | 526 ------------------------- 15 files changed, 664 insertions(+), 909 deletions(-) delete mode 100644 docker/python/Pipfile delete mode 100644 docker/python/Pipfile.lock delete mode 100644 docker/python/requirements.txt create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 standard.rc diff --git a/.circleci/config.yml b/.circleci/config.yml index eed3d2d..8cafe1c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: '2.1' executors: python: docker: - - image: python:3.10 + - image: python:3.13 name: python - image: mysql:8.0 name: mysql @@ -20,13 +20,13 @@ commands: steps: - checkout - run: - name: "Install requirements for checkout" + name: "Install system dependencies" command: | apt update && apt install -y netcat-traditional default-mysql-client git openssh-client curl make nano libzip-dev - run: name: "Install required Python packages" command: | - cd docker/python && pip3 install --no-cache-dir -r requirements.txt && pip3 install --force-reinstall pylint==2.13.9 && cd && cd repo + pip install poetry && poetry config virtualenvs.create false && poetry install --no-root - run: name: "Create application configuration" command: | @@ -59,10 +59,10 @@ jobs: linter: executor: python steps: - - extra_checkout + - extra_checkout - run: name: Run pylint - command: pylint --rcfile=standard.rc src/ ./app.py + command: pylint src/ ./app.py workflows: version: '2.1' diff --git a/.gitignore b/.gitignore index 24d7f37..a58c8c0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ test/**/*.pyc configuration.json .history/ .vscode/ +.venv/ diff --git a/Makefile b/Makefile index 9e26d96..7e271a5 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ test: make import_db && docker compose exec python bash -c 'make test_command_python' linter: - docker compose exec python pylint --rcfile=standard.rc src/ ./app.py + docker compose exec python bash -c 'cd /code && poetry run pylint src/ ./app.py' start: docker compose up @@ -21,9 +21,9 @@ import_db: export_db: docker compose exec mysql bash -c 'cd /code && mysqldump -u game -pazerty games > test/games_test.sql' -# updates the requirements from PIPENV (need to rebuild the pyton container after that) +# updates dependencies and regenerates the lock file (need to rebuild the python container after that) requirements: - docker compose exec python bash -c "cd docker/python && pipenv lock -r > ./requirements.txt" + docker compose exec python bash -c "cd /code && poetry update" ## Containers internal command import_db_command: diff --git a/app.py b/app.py index faac81c..08237aa 100644 --- a/app.py +++ b/app.py @@ -14,7 +14,7 @@ from src.connection.mysql_factory import MySQLFactory app = Flask(__name__) -app.config['JSON_SORT_KEYS'] = False +app.json.sort_keys = False ############## # Load config diff --git a/docker-compose.yml b/docker-compose.yml index 4a059a5..b5414dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,9 @@ services: - python python: - build: docker/python + build: + context: . + dockerfile: docker/python/Dockerfile container_name: python_gmg working_dir: /code volumes: diff --git a/docker/python/Dockerfile b/docker/python/Dockerfile index 60173cd..0048c99 100644 --- a/docker/python/Dockerfile +++ b/docker/python/Dockerfile @@ -1,18 +1,13 @@ -FROM python:3.10 +FROM python:3.13 # expose to nginx EXPOSE 9000 -# ptvsd (VisualStudio debug) -EXPOSE 3000 +RUN pip install poetry -# Install the package manager pipenv -RUN pip install pipenv - -COPY requirements.txt . -RUN pip3 install --no-cache-dir -r requirements.txt - -RUN pip install pylint==2.13.9 +WORKDIR /code +COPY pyproject.toml poetry.lock* ./ +RUN poetry config virtualenvs.create false && poetry install --no-root # --reload: in dev mode, ask gunicorn to restart worker on any source file change CMD ["gunicorn", "--workers=1", "--threads=2", "--bind=0.0.0.0:9000", "--reload", "app:app"] diff --git a/docker/python/Pipfile b/docker/python/Pipfile deleted file mode 100644 index 09ede01..0000000 --- a/docker/python/Pipfile +++ /dev/null @@ -1,17 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -flask = "2.2.5" -ptvsd = "*" -gunicorn = "*" -requests = "*" -mysql-connector-python = "*" -unittest2 = "*" - -[dev-packages] - -[requires] -python_version = "3.7" diff --git a/docker/python/Pipfile.lock b/docker/python/Pipfile.lock deleted file mode 100644 index 4416d74..0000000 --- a/docker/python/Pipfile.lock +++ /dev/null @@ -1,308 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "107c6b99e530abcc701f8af7d20afd4eb157edfdf39a5a57bbdcaae81be3a643" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "argparse": { - "hashes": [ - "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4", - "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314" - ], - "version": "==1.4.0" - }, - "certifi": { - "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.12.7" - }, - "charset-normalizer": { - "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" - ], - "markers": "python_version >= '3'", - "version": "==2.0.12" - }, - "click": { - "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.3" - }, - "flask": { - "hashes": [ - "sha256:58107ed83443e86067e41eff4631b058178191a355886f8e479e347fa1285fdf", - "sha256:edee9b0a7ff26621bd5a8c10ff484ae28737a2410d99b0bb9a6850c7fb977aa0" - ], - "index": "pypi", - "version": "==2.2.5" - }, - "gunicorn": { - "hashes": [ - "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", - "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" - ], - "index": "pypi", - "version": "==20.1.0" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3'", - "version": "==3.4" - }, - "importlib-metadata": { - "hashes": [ - "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed", - "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705" - ], - "markers": "python_version < '3.10'", - "version": "==6.6.0" - }, - "itsdangerous": { - "hashes": [ - "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44", - "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" - }, - "jinja2": { - "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.2" - }, - "linecache2": { - "hashes": [ - "sha256:4b26ff4e7110db76eeb6f5a7b64a82623839d595c2038eeda662f2a2db78e97c", - "sha256:e78be9c0a0dfcbac712fe04fbf92b96cddae80b1b842f24248214c8496f006ef" - ], - "version": "==1.0.0" - }, - "markupsafe": { - "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" - ], - "markers": "python_version >= '3.7'", - "version": "==2.1.2" - }, - "mysql-connector-python": { - "hashes": [ - "sha256:047420715bbb51d3cba78de446c8a6db4666459cd23e168568009c620a3f5b90", - "sha256:1bef2a4a2b529c6e9c46414100ab7032c252244e8a9e017d2b6a41bb9cea9312", - "sha256:245087999f081b389d66621f2abfe2463e3927f63c7c4c0f70ce0f82786ccb93", - "sha256:29ec05ded856b4da4e47239f38489c03b31673ae0f46a090d0e4e29c670e6181", - "sha256:4de5959e27038cbd11dfccb1afaa2fd258c013e59d3e15709dd1992086103050", - "sha256:5eef51e48b22aadd633563bbdaf02112d98d954a4ead53f72fde283ea3f88152", - "sha256:6e2267ad75b37b5e1c480cde77cdc4f795427a54266ead30aabcdbf75ac70064", - "sha256:7be3aeff73b85eab3af2a1e80c053a98cbcb99e142192e551ebd4c1e41ce2596", - "sha256:895135cde57622edf48e1fce3beb4ed85f18332430d48f5c1d9630d49f7712b0", - "sha256:89597c091c4f25b6e023cbbcd32be73affbb0b44256761fe3b8e1d4b14d14d02", - "sha256:a7fd6a71df824f5a7d9a94060598d67b3a32eeccdc9837ee2cd98a44e2536cae", - "sha256:ab0e9d9b5fc114b78dfa9c74e8bfa30b48fcfa17dbb9241ad6faada08a589900", - "sha256:b7dccd7f72f19c97b58428ebf8e709e24eb7e9b67a408af7e77b60efde44bea4", - "sha256:bed43ea3a11f8d4e7c2e3f20c891214e68b45451314f91fddf9ca701de7a53ac", - "sha256:d5afb766b379111942d4260f29499f93355823c7241926471d843c9281fe477c", - "sha256:f353893481476a537cca7afd4e81e0ed84dd2173932b7f1721ab3e3351cbf324", - "sha256:fd608c288f596c4c8767d9a8e90f129385bd19ee6e3adaf6974ad8012c6138b8", - "sha256:fdd262d8538aa504475f8860cfda939a297d3b213c8d15f7ceed52508aeb2aa3" - ], - "index": "pypi", - "version": "==8.0.29" - }, - "protobuf": { - "hashes": [ - "sha256:13233ee2b9d3bd9a5f216c1fa2c321cd564b93d8f2e4f521a85b585447747997", - "sha256:23452f2fdea754a8251d0fc88c0317735ae47217e0d27bf330a30eec2848811a", - "sha256:52f0a78141078077cfe15fe333ac3e3a077420b9a3f5d1bf9b5fe9d286b4d881", - "sha256:70659847ee57a5262a65954538088a1d72dfc3e9882695cab9f0c54ffe71663b", - "sha256:7760730063329d42a9d4c4573b804289b738d4931e363ffbe684716b796bde51", - "sha256:7cf56e31907c532e460bb62010a513408e6cdf5b03fb2611e4b67ed398ad046d", - "sha256:8b54f56d13ae4a3ec140076c9d937221f887c8f64954673d46f63751209e839a", - "sha256:d14fc1a41d1a1909998e8aff7e80d2a7ae14772c4a70e4bf7db8a36690b54425", - "sha256:d4b66266965598ff4c291416be429cef7989d8fae88b55b62095a2331511b3fa", - "sha256:e0e630d8e6a79f48c557cd1835865b593d0547dce221c66ed1b827de59c66c97", - "sha256:ecae944c6c2ce50dda6bf76ef5496196aeb1b85acb95df5843cd812615ec4b61", - "sha256:f08aa300b67f1c012100d8eb62d47129e53d1150f4469fd78a29fa3cb68c66f2", - "sha256:f2f4710543abec186aee332d6852ef5ae7ce2e9e807a3da570f36de5a732d88e" - ], - "markers": "python_version >= '3.7'", - "version": "==4.22.3" - }, - "ptvsd": { - "hashes": [ - "sha256:10745fbb788001959b4de405198d8bd5243611a88fb5a2e2c6800245bc0ddd74", - "sha256:1d3d82ecc82186d099992a748556e6e54037f5c5e4d3fc9bba3e2302354be0d4", - "sha256:20f48ffed42a6beb879c250d82662e175ad59cc46a29c95c6a4472ae413199c5", - "sha256:22b699369a18ff28d4d1aa6a452739e50c7b7790cb16c6312d766e023c12fe27", - "sha256:2bbc121bce3608501998afbe742f02b80e7d26b8fecd38f78b903f22f52a81d9", - "sha256:3b05c06018fdbce5943c50fb0baac695b5c11326f9e21a5266c854306bda28ab", - "sha256:3f839fe91d9ddca0d6a3a0afd6a1c824be1768498a737ab9333d084c5c3f3591", - "sha256:459137736068bb02515040b2ed2738169cb30d69a38e0fd5dffcba255f41e68d", - "sha256:58508485a1609a495dd45829bd6d219303cf9edef5ca1f01a9ed8ffaa87f390c", - "sha256:612948a045fcf9c8931cd306972902440278f34de7ca684b49d4caeec9f1ec62", - "sha256:70260b4591c07bff95566d49b6a5dc3051d8558035c43c847bad9a954def46bb", - "sha256:72d114baa5737baf29c8068d1ccdd93cbb332d2030601c888eed0e3761b588d7", - "sha256:90cbd082e7a9089664888d0d94aca760202f080133fca8f3fe65c48ed6b9e39d", - "sha256:92d26aa7c8f7ffe41cb4b50a00846027027fa17acdf2d9dd8c24de77b25166c6", - "sha256:b9970e3dc987eb2a6001af6c9d2f726dd6455cfc6d47e0f51925cbdee7ea2157", - "sha256:c01204e3f025c3f7252c79c1a8a028246d29e3ef339e1a01ddf652999f47bdea", - "sha256:c893fb9d1c2ef8f980cc00ced3fd90356f86d9f59b58ee97e0e7e622b8860f76", - "sha256:c97c71835dde7e67fc7b06398bee1c012559a0784ebda9cf8acaf176c7ae766c", - "sha256:ccc5c533135305709461f545feed5061c608714db38fa0f58e3f848a127b7fde", - "sha256:cf09fd4d90c4c42ddd9bf853290f1a80bc2128993a3923bd3b96b68cc1acd03f", - "sha256:d2662ec37ee049c0f8f2f9a378abeb7e570d9215c19eaf0a6d7189464195009f", - "sha256:d9337ebba4d099698982e090b203e85670086c4b29cf1185b2e45cd353a8053e", - "sha256:de5234bec74c47da668e1a1a21bcc9821af0cbb28b5153df78cd5abc744b29a2", - "sha256:eda10ecd43daacc180a6fbe524992be76a877c3559e2b78016b4ada8fec10273", - "sha256:fad06de012a78f277318d0c308dd3d7cc1f67167f3b2e1e2f7c6caf04c03440c" - ], - "index": "pypi", - "version": "==4.3.2" - }, - "requests": { - "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" - ], - "index": "pypi", - "version": "==2.27.1" - }, - "setuptools": { - "hashes": [ - "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", - "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" - ], - "markers": "python_version >= '3.7'", - "version": "==67.7.2" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "traceback2": { - "hashes": [ - "sha256:05acc67a09980c2ecfedd3423f7ae0104839eccb55fc645773e1caa0951c3030", - "sha256:8253cebec4b19094d67cc5ed5af99bf1dba1285292226e98a31929f87a5d6b23" - ], - "version": "==1.4.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", - "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" - ], - "markers": "python_version < '3.8'", - "version": "==4.5.0" - }, - "unittest2": { - "hashes": [ - "sha256:13f77d0875db6d9b435e1d4f41e74ad4cc2eb6e1d5c824996092b3430f088bb8", - "sha256:22882a0e418c284e1f718a822b3b022944d53d2d908e1690b319a9d3eb2c0579" - ], - "index": "pypi", - "version": "==1.1.0" - }, - "urllib3": { - "hashes": [ - "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", - "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.15" - }, - "werkzeug": { - "hashes": [ - "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", - "sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612" - ], - "markers": "python_version >= '3.7'", - "version": "==2.2.3" - }, - "zipp": { - "hashes": [ - "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", - "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" - ], - "markers": "python_version >= '3.7'", - "version": "==3.15.0" - } - }, - "develop": {} -} diff --git a/docker/python/requirements.txt b/docker/python/requirements.txt deleted file mode 100644 index b04c22d..0000000 --- a/docker/python/requirements.txt +++ /dev/null @@ -1,32 +0,0 @@ -# -# These requirements were autogenerated by pipenv -# To regenerate from the project's Pipfile, run: -# -# pipenv lock --requirements -# - --i https://pypi.org/simple/ -argparse==1.4.0 -certifi==2023.7.22; python_version >= '3.6' -charset-normalizer==2.0.12; python_version >= '3' -click==8.1.3; python_version >= '3.7' -flask==2.2.5 -gunicorn==23.0.0 -idna==3.7; python_version >= '3' -importlib-metadata==6.6.0; python_version < '3.10' -itsdangerous==2.1.2; python_version >= '3.7' -jinja2==3.1.4; python_version >= '3.7' -linecache2==1.0.0 -markupsafe==2.1.2; python_version >= '3.7' -mysql-connector-python==8.4.0 -protobuf==4.22.3; python_version >= '3.7' -ptvsd==4.3.2 -requests==2.32.0 -setuptools==67.7.2; python_version >= '3.7' -six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' -traceback2==1.4.0 -typing-extensions==4.5.0; python_version < '3.8' -unittest2==1.1.0 -urllib3==1.26.19; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' -werkzeug==3.0.6; python_version >= '3.7' -zipp==3.19.1; python_version >= '3.7' diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..3446640 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,605 @@ +# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand. + +[[package]] +name = "astroid" +version = "3.3.11" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, + {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, +] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, + {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, + {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, +] + +[[package]] +name = "click" +version = "8.3.2" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"}, + {file = "click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "dill" +version = "0.4.1" +description = "serialize all of Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, + {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + +[[package]] +name = "flask" +version = "3.1.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"}, + {file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"}, +] + +[package.dependencies] +blinker = ">=1.9.0" +click = ">=8.1.3" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "gunicorn" +version = "23.0.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "isort" +version = "6.1.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"}, + {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mysql-connector-python" +version = "9.6.0" +description = "A self-contained Python driver for communicating with MySQL servers, using an API that is compliant with the Python Database API Specification v2.0 (PEP 249)." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "mysql_connector_python-9.6.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:478e035ebcf734b3a1497bfd3eb72ce3632da6384545b08cf6329471b3849b6e"}, + {file = "mysql_connector_python-9.6.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:228000bb951810dad724821d04000174ffcc7fa94b4dcef884b17a3cdae07283"}, + {file = "mysql_connector_python-9.6.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:477e86182aefbf693b71ff8bda7679ab4487af64c027759af831a70080aaaeac"}, + {file = "mysql_connector_python-9.6.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:4bf932724a2702d8b9cde4bf764b843a35e85c59479a870997d37a2a68a5632d"}, + {file = "mysql_connector_python-9.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:b507372c060eb39e2a10ebcb3eb1b12e1778b0120808062fc23e3856268cd2d9"}, + {file = "mysql_connector_python-9.6.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:011931f7392a1087e10d305b0303f2a20cc1af2c1c8a15cd5691609aa95dfcbd"}, + {file = "mysql_connector_python-9.6.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b5212372aff6833473d2560ac87d3df9fb2498d0faacb7ebf231d947175fa36a"}, + {file = "mysql_connector_python-9.6.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:61deca6e243fafbb3cf08ae27bd0c83d0f8188de8456e46aeba0d3db15bb7230"}, + {file = "mysql_connector_python-9.6.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:adabbc5e1475cdf5fb6f1902a25edc3bd1e0726fa45f01ab1b8f479ff43b3337"}, + {file = "mysql_connector_python-9.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:8732ca0b7417b45238bcbfc7e64d9c4d62c759672207c6284f0921c366efddc7"}, + {file = "mysql_connector_python-9.6.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9664e217c72dd6fb700f4c8512af90261f72d2f5d7c00c4e13e4c1e09bfa3d5e"}, + {file = "mysql_connector_python-9.6.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:1ed4b5c4761e5333035293e746683890e4ef2e818e515d14023fd80293bc31fa"}, + {file = "mysql_connector_python-9.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5095758dcb89a6bce2379f349da336c268c407129002b595c5dba82ce387e2a5"}, + {file = "mysql_connector_python-9.6.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ae4e7780fad950a4f267dea5851048d160f5b71314a342cdbf30b154f1c74f7"}, + {file = "mysql_connector_python-9.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c180e0b4100d7402e03993bfac5c97d18e01d7ca9d198d742fffc245077f8ffe"}, + {file = "mysql_connector_python-9.6.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e86e45a7b540ca09af8a18ecfa761e0cdeccfdb62818331614ec030ae44bfd26"}, + {file = "mysql_connector_python-9.6.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:8d3e9252384e1b7f95b07020664f2673d9c29c5e95eeda2e048b3331e190b9d4"}, + {file = "mysql_connector_python-9.6.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:0fa18ead33cb699ea92005695077cef09aa494eebf51164ee30c891c3eaea90c"}, + {file = "mysql_connector_python-9.6.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a26490cb029bf7b18a1d2093101105b3526a1036b51ad01553d30138f5beb8d2"}, + {file = "mysql_connector_python-9.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:3460ed976e1b88b7284335d9397a3c519dff56d71580ca1f76ff1c0c7714c813"}, + {file = "mysql_connector_python-9.6.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e2cc13cd3dcdb845d636e52c4e7a9509b63da09bec6ce1b3696be53a79847e2d"}, + {file = "mysql_connector_python-9.6.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:a08c2149d4b52a010c4353f18c84716d18114a4ecd00b466ea34138de2c640f2"}, + {file = "mysql_connector_python-9.6.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b00228b985edd208b20f45c5e684c54e08e31e01bc1d8c3c18a36641c3be5bf7"}, + {file = "mysql_connector_python-9.6.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4617ef5216da7ca32dd46afda61a1552807762434127413bba46fbe4379f59d4"}, + {file = "mysql_connector_python-9.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:bc782f64ca00b6b933d4c6a35568f1349d115cc4434c849b5b9edc015bee3e62"}, + {file = "mysql_connector_python-9.6.0-py2.py3-none-any.whl", hash = "sha256:44b0fb57207ebc6ae05b5b21b7968a9ed33b29187fe87b38951bad2a334d75d5"}, + {file = "mysql_connector_python-9.6.0.tar.gz", hash = "sha256:c453bb55347174d87504b534246fb10c589daf5d057515bf615627198a3c7ef1"}, +] + +[package.extras] +dns-srv = ["dnspython (==2.6.1)"] +gssapi = ["gssapi (==1.8.3)"] +telemetry = ["opentelemetry-api (==1.33.1)", "opentelemetry-exporter-otlp-proto-http (==1.33.1)", "opentelemetry-sdk (==1.33.1)"] +webauthn = ["fido2 (==1.1.2)"] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, +] + +[[package]] +name = "pylint" +version = "3.3.9" +description = "python code static checker" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "pylint-3.3.9-py3-none-any.whl", hash = "sha256:01f9b0462c7730f94786c283f3e52a1fbdf0494bbe0971a78d7277ef46a751e7"}, + {file = "pylint-3.3.9.tar.gz", hash = "sha256:d312737d7b25ccf6b01cc4ac629b5dcd14a0fcf3ec392735ac70f137a9d5f83a"}, +] + +[package.dependencies] +astroid = ">=3.3.8,<=3.4.0.dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} +isort = ">=4.2.5,<5.13 || >5.13,<7" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "requests" +version = "2.33.1" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, + {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + +[[package]] +name = "tomlkit" +version = "0.14.0" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, + {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "werkzeug" +version = "3.1.8" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50"}, + {file = "werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"}, +] + +[package.dependencies] +markupsafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.13" +content-hash = "fe01a4f88916ff264c5392f41ac945f7a1e02ff964619bfb885626370fddbaf7" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6b0492a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[tool.poetry] +name = "gmg-back" +version = "1.0.0" +description = "Give Me a Game - Video game collection management API" +authors = ["Eric COURTIAL"] +package-mode = false + +[tool.poetry.dependencies] +python = "^3.13" +flask = "^3.1" +gunicorn = "^23.0" +mysql-connector-python = "^9.0" + +[tool.poetry.group.dev.dependencies] +pylint = "^3.3" +requests = "^2.32" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pylint.main] +fail-under = 10 + +[tool.pylint."messages control"] +enable = ["c-extension-no-member"] +disable = ["C0114", "C0115", "C0116", "R0801", "W0622", "R0914", "R0902", "R0904", "R0913", "W0707", "R0917"] + +[tool.pylint.reports] +evaluation = "10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)" + +[tool.pylint.format] +max-line-length = 200 + +[tool.pylint.basic] +bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] diff --git a/src/repository/story_repository.py b/src/repository/story_repository.py index a5df692..95b2db8 100644 --- a/src/repository/story_repository.py +++ b/src/repository/story_repository.py @@ -25,4 +25,3 @@ def hydrate(self, row): story.set_game_title(row['gameTitle']) return story - \ No newline at end of file diff --git a/src/service/abstract_service.py b/src/service/abstract_service.py index ec515c9..0dc4ef8 100644 --- a/src/service/abstract_service.py +++ b/src/service/abstract_service.py @@ -3,7 +3,7 @@ from src.exception.unsupported_value_exception import UnsupportedValueException from src.helpers.json_helper import JsonHelper -class AbstractService: # pylint: disable=no-member,no-self-use +class AbstractService: # pylint: disable=no-member def get_by_id(self, entity_id): object = self.repository.get_by_id(entity_id) diff --git a/src/service/transaction_service.py b/src/service/transaction_service.py index 8ed2aa1..59b93c5 100644 --- a/src/service/transaction_service.py +++ b/src/service/transaction_service.py @@ -92,11 +92,11 @@ def get_copy(self, transaction): return None - def check_version_copy_consistency(self, version, copy): #pylint: disable=no-self-use + def check_version_copy_consistency(self, version, copy): if (copy is not None and version.get_id() != copy.get_version_id()): raise InconsistentVersionAndCopyIdException(version.get_id(), copy.get_version_id()) - def update_copy_status(self, transaction, copy): #pylint: disable=no-self-use + def update_copy_status(self, transaction, copy): if copy is not None: # You cannot create an outbound transaction if you already don't have the copy anymore if transaction.get_type() in transaction.transaction_out and copy.get_status() == 'Out': @@ -107,7 +107,7 @@ def update_copy_status(self, transaction, copy): #pylint: disable=no-self-use else: copy.set_status('Out') - def update_copy_in_db(self, transaction, copy): #pylint: disable=no-self-use + def update_copy_in_db(self, transaction, copy): if copy is not None: if self.is_sold_transaction(transaction): self.repository.reset_copy_id_for_transactions(copy.get_id(), False) @@ -115,7 +115,7 @@ def update_copy_in_db(self, transaction, copy): #pylint: disable=no-self-use else: self.copy_repository.update(copy) - def is_sold_transaction(self, transaction): #pylint: disable=no-self-use + def is_sold_transaction(self, transaction): if transaction.get_type() == 'Sold': return True diff --git a/standard.rc b/standard.rc deleted file mode 100644 index f15c425..0000000 --- a/standard.rc +++ /dev/null @@ -1,526 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10 - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=200 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. - - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception - -# C0114,C0115,C0116 are for doc -# R0801 is for duplicated code -# W0622 is for redefining a variable -# R0914 is for too many local variables -# R0902 is for too many instance attributes -# R0904 is for too many public methods -# R0913 is for too many arguments in a function -# W0707: Consider explicitly re-raising using the 'from' keyword (raise-missing-from) - -disable=C0114,C0115,C0116,R0801,W0622,R0914,R0902,R0904,R0913,W0707 From b707acba10464a0bb24e3b4c8226d3c719d49b3a Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Wed, 8 Apr 2026 22:24:22 +0200 Subject: [PATCH 03/18] added ruff --- Makefile | 2 +- poetry.lock | 30 ++++++++++++++++++- pyproject.toml | 1 + .../invalid_credentials_exception.py | 2 +- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 7e271a5..f058fda 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ test: make import_db && docker compose exec python bash -c 'make test_command_python' linter: - docker compose exec python bash -c 'cd /code && poetry run pylint src/ ./app.py' + docker compose exec python bash -c 'cd /code && poetry run ruff check src && poetry run pylint src/ ./app.py' start: docker compose up diff --git a/poetry.lock b/poetry.lock index 3446640..952ecd9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -551,6 +551,34 @@ urllib3 = ">=1.26,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] +[[package]] +name = "ruff" +version = "0.9.10" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d"}, + {file = "ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d"}, + {file = "ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1"}, + {file = "ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c"}, + {file = "ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43"}, + {file = "ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c"}, + {file = "ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5"}, + {file = "ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8"}, + {file = "ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029"}, + {file = "ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1"}, + {file = "ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69"}, + {file = "ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7"}, +] + [[package]] name = "tomlkit" version = "0.14.0" @@ -602,4 +630,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "fe01a4f88916ff264c5392f41ac945f7a1e02ff964619bfb885626370fddbaf7" +content-hash = "2e00a6237dbca1b45f99f943147194a7150b57e612fba6f39b27f198c44cb9cf" diff --git a/pyproject.toml b/pyproject.toml index 6b0492a..b2c0296 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ mysql-connector-python = "^9.0" [tool.poetry.group.dev.dependencies] pylint = "^3.3" +ruff = "^0.9" requests = "^2.32" [build-system] diff --git a/src/exception/invalid_credentials_exception.py b/src/exception/invalid_credentials_exception.py index 539e0c7..8f26104 100644 --- a/src/exception/invalid_credentials_exception.py +++ b/src/exception/invalid_credentials_exception.py @@ -1,6 +1,6 @@ class InvalidCredentialsException(Exception): def __init__(self): - super().__init__(f"The credentials are invalid.") + super().__init__("The credentials are invalid.") def get_code(self): return 2 From f81b48a2598257e169c64f0c8276a672df1791fb Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Thu, 9 Apr 2026 20:23:50 +0200 Subject: [PATCH 04/18] Strong typing and Ruff --- README.md | 2 +- app.py | 91 ++++----- pyproject.toml | 7 + src/connection/mysql_factory.py | 11 +- src/controller/abstract_controller.py | 13 +- src/controller/user_controller.py | 14 +- src/entity/abstract_entity.py | 13 +- src/entity/copy.py | 127 ++++++------ src/entity/game.py | 38 ++-- src/entity/note.py | 30 ++- src/entity/platform.py | 23 +-- src/entity/story.py | 61 +++--- src/entity/transaction.py | 78 ++++---- src/entity/user.py | 37 ++-- src/entity/version.py | 187 +++++++++--------- .../duplicate_consecutive_operation.py | 4 +- src/exception/inactive_user_exception.py | 4 +- src/exception/inconsistent_operation.py | 2 +- ...inconsistent_transaction_type_operation.py | 4 +- .../inconsistent_version_and_copy_id.py | 4 +- .../invalid_credentials_exception.py | 4 +- src/exception/invalid_input.py | 2 +- src/exception/missing_field_exception.py | 4 +- src/exception/missing_header_exception.py | 4 +- .../resource_already_exists_exception.py | 6 +- .../resource_has_children_exception.py | 4 +- src/exception/unknown_resource_exception.py | 8 +- src/exception/unsupported_filter_exception.py | 4 +- src/exception/unsupported_value_exception.py | 4 +- src/helpers/json_helper.py | 7 +- src/repository/abstract_core_repository.py | 18 +- src/repository/abstract_repository.py | 55 +++--- src/repository/copy_repository.py | 7 +- src/repository/game_repository.py | 9 +- src/repository/note_repository.py | 9 +- src/repository/platform_repository.py | 9 +- src/repository/story_repository.py | 7 +- src/repository/transaction_repository.py | 13 +- src/repository/user_repository.py | 15 +- src/repository/version_repository.py | 9 +- src/service/abstract_service.py | 19 +- src/service/copy_service.py | 12 +- src/service/game_service.py | 11 +- src/service/note_service.py | 16 +- src/service/platform_service.py | 11 +- src/service/story_service.py | 12 +- src/service/transaction_service.py | 33 ++-- src/service/user_service.py | 38 ++-- src/service/version_service.py | 11 +- 49 files changed, 588 insertions(+), 523 deletions(-) diff --git a/README.md b/README.md index 2997b21..f1e3a5b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ I did not include a GUI because: The developer who want to use this application is free to develop it's own front app connected through the REST endpoints, it is a classic. You can create a classy shiny state of the art front app or just a basic one using only one part of the features the back-end offers. -However, I developed my own front app, available [here](https://github.com/ecourtial/gmg-front), using PHP 8.1 and Symfony. You can use it if you don't have specific needs. Note: it does not include the support for all the features given by the back application. +However, I developed my own front app, available [here](https://github.com/ecourtial/gmg-front), using PHP and Symfony. You can use it if you don't have specific needs. Note: it does not include the support for all the features given by the back application, but almost everything though. ## Utilization diff --git a/app.py b/app.py index 08237aa..7f06353 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,8 @@ """Main file of the app. Loaded once on server startup!""" from functools import wraps import json -from flask import Flask, jsonify, request +from typing import Any, Callable +from flask import Flask, jsonify, request, Response from src.controller.platform_controller import PlatformController from src.controller.game_controller import GameController from src.controller.user_controller import UserController @@ -38,9 +39,9 @@ # User management ################## -def token_required(decorated_function): +def token_required(decorated_function: Callable[..., Any]) -> Callable[..., Any]: @wraps(decorated_function) - def decorator(*args, **kwargs): + def decorator(*args: Any, **kwargs: Any) -> tuple[Response, int] | Any: token = None @@ -71,7 +72,7 @@ def decorator(*args, **kwargs): ######################################################################## @app.after_request -def after_request(response): +def after_request(response: Response) -> Response: """Handle logic after each request""" response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, public, max-age=0" response.headers["Expires"] = 0 @@ -88,21 +89,21 @@ def after_request(response): # Home @app.route('/') -def home(): +def home() -> tuple[Response, int]: """Homepage with layout""" return jsonify({'message': 'Hello!'}), 200 # Users @app.route('/api/v1/user/authenticate', methods=['POST']) -def authenticate_user(): +def authenticate_user() -> tuple[Response, int]: """Returns the token of the given user""" controller = UserController return controller.authenticate(MySQLFactory.get()) @app.route('/api/v1/user', methods=['GET']) @token_required -def get_user(): +def get_user() -> tuple[Response, int]: """Returns the user according to one filter""" controller = UserController return controller.get_by_filter( @@ -113,21 +114,21 @@ def get_user(): @app.route('/api/v1/user', methods=['POST']) @token_required -def create_user(): +def create_user() -> tuple[Response, int]: """Creates a user""" controller = UserController return controller.create(MySQLFactory.get()) @app.route('/api/v1/user/', methods=['PATCH']) @token_required -def update_user(entity_id): +def update_user(entity_id: int) -> tuple[Response, int]: """Updates a user""" controller = UserController return controller.update(MySQLFactory.get(), entity_id) @app.route('/api/v1/user/renew-token', methods=['POST']) @token_required -def renew_token(current_user): +def renew_token(current_user: Any) -> tuple[Response, int]: """Renew the API token of the current user""" controller = UserController return controller.renew_token(MySQLFactory.get(), current_user) @@ -135,34 +136,34 @@ def renew_token(current_user): # Platforms @app.route('/api/v1/platform/', methods=['GET']) -def get_platform_by_id(entity_id): +def get_platform_by_id(entity_id: int) -> tuple[Response, int]: """Returns the platform according to its id""" controller = PlatformController return controller.get_by_id(MySQLFactory.get(), entity_id) @app.route('/api/v1/platform', methods=['POST']) @token_required -def create_platform(): +def create_platform() -> tuple[Response, int]: """Create a platform""" controller = PlatformController return controller.create(MySQLFactory.get()) @app.route('/api/v1/platform/', methods=['PATCH']) @token_required -def update_platform(entity_id): +def update_platform(entity_id: int) -> tuple[Response, int]: """Update the platform according to its id""" controller = PlatformController return controller.update(MySQLFactory.get(), entity_id) @app.route('/api/v1/platform/', methods=['DELETE']) @token_required -def delete_platform(entity_id): +def delete_platform(entity_id: int) -> tuple[Response, int]: """Delete the platform according to its id""" controller = PlatformController return controller.delete(MySQLFactory.get(), entity_id) @app.route('/api/v1/platforms', methods=['GET']) -def get_platforms(): +def get_platforms() -> Response: """Get the platforms""" controller = PlatformController return controller.get_list(MySQLFactory.get()) @@ -170,34 +171,34 @@ def get_platforms(): # Games @app.route('/api/v1/game/', methods=['GET']) -def get_game_by_id(entity_id): +def get_game_by_id(entity_id: int) -> tuple[Response, int]: """Returns the game according to its id""" controller = GameController return controller.get_by_id(MySQLFactory.get(), entity_id) @app.route('/api/v1/game', methods=['POST']) @token_required -def create_game(): +def create_game() -> tuple[Response, int]: """Create a game""" controller = GameController return controller.create(MySQLFactory.get()) @app.route('/api/v1/game/', methods=['PATCH']) @token_required -def update_game(entity_id): +def update_game(entity_id: int) -> tuple[Response, int]: """Update the game according to its id""" controller = GameController return controller.update(MySQLFactory.get(), entity_id) @app.route('/api/v1/game/', methods=['DELETE']) @token_required -def delete_game(entity_id): +def delete_game(entity_id: int) -> tuple[Response, int]: """Delete the game according to its id""" controller = GameController return controller.delete(MySQLFactory.get(), entity_id) @app.route('/api/v1/games', methods=['GET']) -def get_games(): +def get_games() -> Response: """Get the games""" controller = GameController return controller.get_list(MySQLFactory.get()) @@ -205,34 +206,34 @@ def get_games(): # Versions @app.route('/api/v1/version/', methods=['GET']) -def get_version_by_id(entity_id): +def get_version_by_id(entity_id: int) -> tuple[Response, int]: """Returns the version according to its id""" controller = VersionController return controller.get_by_id(MySQLFactory.get(), entity_id) @app.route('/api/v1/version', methods=['POST']) @token_required -def create_version(): +def create_version() -> tuple[Response, int]: """Create a version""" controller = VersionController return controller.create(MySQLFactory.get()) @app.route('/api/v1/version/', methods=['PATCH']) @token_required -def update_version(entity_id): +def update_version(entity_id: int) -> tuple[Response, int]: """Update the version according to its id""" controller = VersionController return controller.update(MySQLFactory.get(), entity_id) @app.route('/api/v1/version/', methods=['DELETE']) @token_required -def delete_version(entity_id): +def delete_version(entity_id: int) -> tuple[Response, int]: """Delete the version according to its id""" controller = VersionController return controller.delete(MySQLFactory.get(), entity_id) @app.route('/api/v1/versions', methods=['GET']) -def get_versions(): +def get_versions() -> Response: """Get the versions""" controller = VersionController return controller.get_list(MySQLFactory.get()) @@ -240,34 +241,34 @@ def get_versions(): # Copies @app.route('/api/v1/copy/', methods=['GET']) -def get_copy_by_id(entity_id): +def get_copy_by_id(entity_id: int) -> tuple[Response, int]: """Returns the copy according to its id""" controller = CopyController return controller.get_by_id(MySQLFactory.get(), entity_id) @app.route('/api/v1/copy', methods=['POST']) @token_required -def create_copy(): +def create_copy() -> tuple[Response, int]: """Create a copy""" controller = CopyController return controller.create(MySQLFactory.get()) @app.route('/api/v1/copy/', methods=['PATCH']) @token_required -def update_copy(entity_id): +def update_copy(entity_id: int) -> tuple[Response, int]: """Update the copy according to its id""" controller = CopyController return controller.update(MySQLFactory.get(), entity_id) @app.route('/api/v1/copy/', methods=['DELETE']) @token_required -def delete_copy(entity_id): +def delete_copy(entity_id: int) -> tuple[Response, int]: """Delete the copy according to its id""" controller = CopyController return controller.delete(MySQLFactory.get(), entity_id) @app.route('/api/v1/copies', methods=['GET']) -def get_copies(): +def get_copies() -> Response: """Get the copies""" controller = CopyController return controller.get_list(MySQLFactory.get()) @@ -275,34 +276,34 @@ def get_copies(): # Stories @app.route('/api/v1/story/', methods=['GET']) -def get_story_by_id(entity_id): +def get_story_by_id(entity_id: int) -> tuple[Response, int]: """Returns the story according to its id""" controller = StoryController return controller.get_by_id(MySQLFactory.get(), entity_id) @app.route('/api/v1/story', methods=['POST']) @token_required -def create_story(): +def create_story() -> tuple[Response, int]: """Create a story""" controller = StoryController return controller.create(MySQLFactory.get()) @app.route('/api/v1/story/', methods=['PATCH']) @token_required -def update_story(entity_id): +def update_story(entity_id: int) -> tuple[Response, int]: """Update the story according to its id""" controller = StoryController return controller.update(MySQLFactory.get(), entity_id) @app.route('/api/v1/story/', methods=['DELETE']) @token_required -def delete_story(entity_id): +def delete_story(entity_id: int) -> tuple[Response, int]: """Delete the story according to its id""" controller = StoryController return controller.delete(MySQLFactory.get(), entity_id) @app.route('/api/v1/stories', methods=['GET']) -def get_stories(): +def get_stories() -> Response: """Get the stories""" controller = StoryController return controller.get_list(MySQLFactory.get()) @@ -310,34 +311,34 @@ def get_stories(): # Transactions @app.route('/api/v1/transaction/', methods=['GET']) -def get_transaction_by_id(entity_id): +def get_transaction_by_id(entity_id: int) -> tuple[Response, int]: """Returns the transaction according to its id""" controller = TransactionController return controller.get_by_id(MySQLFactory.get(), entity_id) @app.route('/api/v1/transaction', methods=['POST']) @token_required -def create_transaction(): +def create_transaction() -> tuple[Response, int]: """Create a transaction""" controller = TransactionController return controller.create(MySQLFactory.get()) @app.route('/api/v1/transaction/', methods=['PATCH']) @token_required -def update_transaction(entity_id): +def update_transaction(entity_id: int) -> tuple[Response, int]: """Update the transaction according to its id""" controller = TransactionController return controller.update(MySQLFactory.get(), entity_id) @app.route('/api/v1/transaction/', methods=['DELETE']) @token_required -def delete_transaction(entity_id): +def delete_transaction(entity_id: int) -> tuple[Response, int]: """Delete the transaction according to its id""" controller = TransactionController return controller.delete(MySQLFactory.get(), entity_id) @app.route('/api/v1/transactions', methods=['GET']) -def get_transactions(): +def get_transactions() -> Response: """Get the transactions""" controller = TransactionController return controller.get_list(MySQLFactory.get()) @@ -345,34 +346,34 @@ def get_transactions(): # Notes @app.route('/api/v1/note/', methods=['GET']) -def get_note_by_id(entity_id): +def get_note_by_id(entity_id: int) -> tuple[Response, int]: """Returns the note according to its id""" controller = NoteController return controller.get_by_id(MySQLFactory.get(), entity_id) @app.route('/api/v1/note', methods=['POST']) @token_required -def create_note(): +def create_note() -> tuple[Response, int]: """Create a note""" controller = NoteController return controller.create(MySQLFactory.get()) @app.route('/api/v1/note/', methods=['PATCH']) @token_required -def update_note(entity_id): +def update_note(entity_id: int) -> tuple[Response, int]: """Update the note according to its id""" controller = NoteController return controller.update(MySQLFactory.get(), entity_id) @app.route('/api/v1/note/', methods=['DELETE']) @token_required -def delete_note(entity_id): +def delete_note(entity_id: int) -> tuple[Response, int]: """Delete the note according to its id""" controller = NoteController return controller.delete(MySQLFactory.get(), entity_id) @app.route('/api/v1/notes', methods=['GET']) -def get_notes(): +def get_notes() -> Response: """Get the notes""" controller = NoteController return controller.get_list(MySQLFactory.get()) diff --git a/pyproject.toml b/pyproject.toml index b2c0296..01e3147 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,3 +35,10 @@ max-line-length = 200 [tool.pylint.basic] bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] + +[tool.ruff] +line-length = 200 + +[tool.ruff.lint] +select = ["E", "F", "ANN"] +ignore = ["ANN401"] diff --git a/src/connection/mysql_factory.py b/src/connection/mysql_factory.py index 06d2053..1c0e714 100644 --- a/src/connection/mysql_factory.py +++ b/src/connection/mysql_factory.py @@ -1,9 +1,12 @@ +from typing import Any + from mysql import connector + class MySQLFactory: - + @classmethod - def init(cls, host, user, password, database): + def init(cls, host: str, user: str, password: str, database: str) -> None: cls.host = host cls.user = user cls.password = password @@ -11,7 +14,7 @@ def init(cls, host, user, password, database): cls.connection = None @classmethod - def get(cls): + def get(cls) -> Any: """Get a connection""" if cls.connection is None: cls.connection = connector.connect( @@ -24,7 +27,7 @@ def get(cls): return cls.connection @classmethod - def close(cls): + def close(cls) -> None: if cls.connection is not None: cls.connection.close() cls.connection = None diff --git a/src/controller/abstract_controller.py b/src/controller/abstract_controller.py index 3824832..c2e64c5 100644 --- a/src/controller/abstract_controller.py +++ b/src/controller/abstract_controller.py @@ -1,4 +1,5 @@ -from flask import request, jsonify +from typing import Any +from flask import request, jsonify, Response from src.exception.inconsistent_operation import InconsistentOperation from src.exception.missing_field_exception import MissingFieldException from src.exception.resource_already_exists_exception import ResourceAlreadyExistsException @@ -8,7 +9,7 @@ class AbstractController:# pylint: disable=no-member @classmethod - def get_by_id(cls, mysql, entity_id): + def get_by_id(cls, mysql: Any, entity_id: int) -> tuple[Response, int]: service = cls.service(mysql) try: @@ -19,7 +20,7 @@ def get_by_id(cls, mysql, entity_id): return jsonify(copy.serialize()), 200 @classmethod - def create(cls, mysql): + def create(cls, mysql: Any) -> tuple[Response, int]: service = cls.service(mysql) try: @@ -39,7 +40,7 @@ def create(cls, mysql): return cls.get_by_id(mysql, object.get_id()) @classmethod - def update(cls, mysql, entity_id): + def update(cls, mysql: Any, entity_id: int) -> tuple[Response, int]: service = cls.service(mysql) try: @@ -57,7 +58,7 @@ def update(cls, mysql, entity_id): return cls.get_by_id(mysql, object.get_id()) @classmethod - def delete(cls, mysql, entity_id): + def delete(cls, mysql: Any, entity_id: int) -> tuple[Response, int]: service = cls.service(mysql) try: @@ -70,7 +71,7 @@ def delete(cls, mysql, entity_id): return jsonify({'message': service.resource_type + ' successfully deleted.'}), 200 @classmethod - def get_list(cls, mysql): + def get_list(cls, mysql: Any) -> Response: repo = cls.repository(mysql) page = request.args.get('page', 1) diff --git a/src/controller/user_controller.py b/src/controller/user_controller.py index d5af98f..e0652a8 100644 --- a/src/controller/user_controller.py +++ b/src/controller/user_controller.py @@ -1,5 +1,6 @@ """Controller to handle user operations""" -from flask import jsonify +from typing import Any +from flask import jsonify, Response from src.exception.inactive_user_exception import InactiveUserException from src.exception.invalid_credentials_exception import InvalidCredentialsException from src.exception.invalid_input import InvalidInput @@ -8,12 +9,13 @@ from src.exception.resource_already_exists_exception import ResourceAlreadyExistsException from src.exception.unknown_resource_exception import ResourceNotFoundException from src.exception.unsupported_filter_exception import UnsupportedFilterException +from src.entity.user import User from src.repository.user_repository import UserRepository from src.service.user_service import UserService class UserController:# pylint: disable=R0911 @classmethod - def authenticate(cls, mysql): + def authenticate(cls, mysql: Any) -> tuple[Response, int]: user_service = UserService(mysql) try: @@ -42,7 +44,7 @@ def authenticate(cls, mysql): ), 200 @classmethod - def get_by_filter(cls, mysql, filter, filter_value): + def get_by_filter(cls, mysql: Any, filter: str, filter_value: int | str) -> tuple[Response, int]: user_service = UserService(mysql) try: @@ -62,7 +64,7 @@ def get_by_filter(cls, mysql, filter, filter_value): ), 200 @classmethod - def create(cls, mysql): + def create(cls, mysql: Any) -> tuple[Response, int]: service = UserService(mysql) try: @@ -77,7 +79,7 @@ def create(cls, mysql): return cls.get_by_filter(mysql, 'id', user.get_id()) @classmethod - def update(cls, mysql, user_id): + def update(cls, mysql: Any, user_id: int) -> tuple[Response, int]: service = UserService(mysql) try: @@ -95,7 +97,7 @@ def update(cls, mysql, user_id): return cls.get_by_filter(mysql, 'id', user.get_id()) @classmethod - def renew_token(cls, mysql, current_user): + def renew_token(cls, mysql: Any, current_user: User) -> tuple[Response, int]: user_service = UserService(mysql) user_service.renew_token(current_user) diff --git a/src/entity/abstract_entity.py b/src/entity/abstract_entity.py index 64c06da..a41ad63 100644 --- a/src/entity/abstract_entity.py +++ b/src/entity/abstract_entity.py @@ -1,9 +1,12 @@ -class AbstractEntity: # pylint: disable=too-few-public-methods,E1101 - expected_fields = {} - authorized_extra_fields_for_filtering = {} +from typing import Any - def serialize(self): - values = {} + +class AbstractEntity: # pylint: disable=too-few-public-methods,E1101 + expected_fields: dict[str, Any] = {} + authorized_extra_fields_for_filtering: dict[str, Any] = {} + + def serialize(self) -> dict[str, Any]: + values: dict[str, Any] = {} values['id'] = self.get_id() for api_field, data in self.expected_fields.items(): diff --git a/src/entity/copy.py b/src/entity/copy.py index 6ca8b3f..314b9e5 100644 --- a/src/entity/copy.py +++ b/src/entity/copy.py @@ -1,8 +1,11 @@ """ Copy entity for the GMG project """ +from typing import Any + from src.entity.abstract_entity import AbstractEntity + class Copy(AbstractEntity): - expected_fields = { + expected_fields: dict[str, Any] = { 'versionId': { 'field': 'version_id', 'method': '_version_id', @@ -134,7 +137,7 @@ class Copy(AbstractEntity): }, } - authorized_extra_fields_for_filtering = { + authorized_extra_fields_for_filtering: dict[str, Any] = { 'id': {'field': 'copy_id', 'origin': 'native', 'type': 'int'}, 'transactionCount': {'field': 'transactionCount', 'origin': 'computed', 'type': 'int'}, 'platformName': {'field': 'platformName', 'origin': 'computed', 'type': 'string'}, @@ -144,29 +147,29 @@ class Copy(AbstractEntity): table_name = 'copies' primary_key = 'copy_id' -# If you change the order here, you need to also change it in the array above! + # If you change the order here, you need to also change it in the array above! def __init__( self, - entity_id, - version_id, - is_original, - language, - box_type, - is_box_repro, - casing_type, - support_type, - on_compilation, - is_reedition, - has_manual, - status, - type, - region, - comments, - is_rom = None, - platform_name = None, - game_title = None, - transaction_count = None, - ): + entity_id: int | None, + version_id: int, + is_original: int, + language: str, + box_type: str, + is_box_repro: int, + casing_type: str, + support_type: str, + on_compilation: int, + is_reedition: int, + has_manual: int, + status: str, + type: str, + region: str, + comments: str, + is_rom: int | None = None, + platform_name: str | None = None, + game_title: str | None = None, + transaction_count: int | None = None, + ) -> None: self.entity_id = entity_id self.version_id = int(version_id) self.is_original = bool(is_original) @@ -187,122 +190,120 @@ def __init__( self.game_title = game_title self.transaction_count = int(transaction_count or 0) - def get_id(self): + def get_id(self) -> int | None: return self.entity_id - def get_version_id(self): + def get_version_id(self) -> int: return self.version_id - def get_is_original(self): + def get_is_original(self) -> bool: return self.is_original - def get_language(self): + def get_language(self) -> str: return self.language - def get_box_type(self): + def get_box_type(self) -> str: return self.box_type - def get_is_box_repro(self): + def get_is_box_repro(self) -> bool: return bool(self.is_box_repro) - def get_casing_type(self): + def get_casing_type(self) -> str: return self.casing_type - def get_support_type(self): + def get_support_type(self) -> str: return self.support_type - def get_on_compilation(self): + def get_on_compilation(self) -> bool: return self.on_compilation - def get_is_reedition(self): + def get_is_reedition(self) -> bool: return self.is_reedition - def get_has_manual(self): + def get_has_manual(self) -> bool: return self.has_manual - def get_status(self): + def get_status(self) -> str: return self.status - def get_type(self): + def get_type(self) -> str: return self.type - def get_is_rom(self): + def get_is_rom(self) -> bool: return self.is_rom - def get_region(self): + def get_region(self) -> str: return self.region - def get_comments(self): + def get_comments(self) -> str: return self.comments - def set_version_id(self, version_id): + def set_version_id(self, version_id: int) -> None: self.version_id = version_id - def set_is_original(self, is_original): + def set_is_original(self, is_original: int) -> None: self.is_original = bool(is_original) - def set_language(self, language): + def set_language(self, language: str) -> None: self.language = language - def set_box_type(self, type): + def set_box_type(self, type: str) -> None: self.box_type = type - def set_is_box_repro(self, is_repro): + def set_is_box_repro(self, is_repro: int) -> None: self.is_box_repro = bool(is_repro) - def set_casing_type(self, type): + def set_casing_type(self, type: str) -> None: self.casing_type = type - def set_support_type(self, support_type): + def set_support_type(self, support_type: str) -> None: self.support_type = support_type - def set_on_compilation(self, status): + def set_on_compilation(self, status: int) -> None: self.on_compilation = bool(status) - def set_is_reedition(self, status): + def set_is_reedition(self, status: int) -> None: self.is_reedition = bool(status) - def set_has_manual(self, status): + def set_has_manual(self, status: int) -> None: self.has_manual = bool(status) - def set_status(self, status): + def set_status(self, status: str) -> None: self.status = status - def set_type(self, type): + def set_type(self, type: str) -> None: self.type = type - def set_is_rom(self, is_rom): + def set_is_rom(self, is_rom: int) -> None: self.is_rom = bool(is_rom) - def set_region(self, region): + def set_region(self, region: str) -> None: self.region = region - def set_comments(self, comments): + def set_comments(self, comments: str) -> None: self.comments = comments - def get_game_title(self): + def get_game_title(self) -> str | None: return self.game_title - def set_game_title(self, title): + def set_game_title(self, title: str) -> None: self.game_title = title - def get_platform_name(self): + def get_platform_name(self) -> str | None: return self.platform_name - def set_platform_name(self, platform_name): + def set_platform_name(self, platform_name: str) -> None: self.platform_name = platform_name - def get_transaction_count(self): + def get_transaction_count(self) -> int: return self.transaction_count - def set_transaction_count(self, transaction_count): + def set_transaction_count(self, transaction_count: int | None) -> None: self.transaction_count = int(transaction_count or 0) - def serialize(self): + def serialize(self) -> dict[str, Any]: values = super().serialize() - values['platformName'] = self.get_platform_name() values['gameTitle'] = self.get_game_title() values['transactionCount'] = self.get_transaction_count() - return values diff --git a/src/entity/game.py b/src/entity/game.py index fe56e82..fe032b2 100644 --- a/src/entity/game.py +++ b/src/entity/game.py @@ -1,7 +1,12 @@ +from typing import Any + from src.entity.abstract_entity import AbstractEntity + class Game(AbstractEntity): - expected_fields = { + """ This class represent a game, for instance "The Secret Of Monkey Island" """ + + expected_fields: dict[str, Any] = { 'title': {'field': 'title', 'method': '_title', 'required': True, 'type': 'text'}, 'notes': { 'field': 'notes', @@ -12,7 +17,7 @@ class Game(AbstractEntity): }, } - authorized_extra_fields_for_filtering = { + authorized_extra_fields_for_filtering: dict[str, Any] = { 'id': {'field': 'id', 'origin': 'native', 'type': 'int'}, 'versionCount': {'field': 'versionCount', 'origin': 'computed', 'type': 'int'} } @@ -20,47 +25,36 @@ class Game(AbstractEntity): table_name = 'games' primary_key = 'id' - """ This class represent a game, for instance "The Secret Of Monkey Island" """ - def __init__( - self, - entity_id, - title, - notes, - version_count = None, - ): + def __init__(self, entity_id: int | None, title: str, notes: str, version_count: int | None = None) -> None: self.entity_id = entity_id self.title = title self.notes = notes self.version_count = int(version_count or 0) - def get_id(self): + def get_id(self) -> int | None: """Return the id of the game, for instance "125".""" - return self.entity_id - def get_title(self): + def get_title(self) -> str: """Return the title of the game, for instance "Woodruff and the Schnibble of Azimuth".""" - return self.title - def set_title(self, title): + def set_title(self, title: str) -> None: self.title = title - def get_notes(self): + def get_notes(self) -> str: return self.notes - def set_notes(self, notes): + def set_notes(self, notes: str) -> None: self.notes = notes - def get_version_count(self): + def get_version_count(self) -> int: return self.version_count - def set_version_count(self, version_count): + def set_version_count(self, version_count: int | None) -> None: self.version_count = int(version_count or 0) - def serialize(self): + def serialize(self) -> dict[str, Any]: values = super().serialize() - values['versionCount'] = self.get_version_count() - return values diff --git a/src/entity/note.py b/src/entity/note.py index 493c073..72953be 100644 --- a/src/entity/note.py +++ b/src/entity/note.py @@ -1,7 +1,12 @@ +from typing import Any + from src.entity.abstract_entity import AbstractEntity + class Note(AbstractEntity): - expected_fields = { + """ This class represent a note entry """ + + expected_fields: dict[str, Any] = { 'title': { 'field': 'title', 'method': '_title', @@ -21,33 +26,22 @@ class Note(AbstractEntity): table_name = 'notes' primary_key = 'id' - """ This class represent a note entry """ - def __init__( - self, - entity_id, - title, - content, - ): + def __init__(self, entity_id: int | None, title: str, content: str) -> None: self.entity_id = entity_id self.title = title self.content = content - def get_id(self): + def get_id(self) -> int | None: return self.entity_id - def get_title(self): + def get_title(self) -> str: return self.title - def get_content(self): + def get_content(self) -> str: return self.content - def set_title(self, title): + def set_title(self, title: str) -> None: self.title = title - def set_content(self, content): + def set_content(self, content: str) -> None: self.content = content - - def serialize(self): - values = super().serialize() - - return values diff --git a/src/entity/platform.py b/src/entity/platform.py index 4f921c7..2adb199 100644 --- a/src/entity/platform.py +++ b/src/entity/platform.py @@ -1,13 +1,16 @@ +from typing import Any + from src.entity.abstract_entity import AbstractEntity + class Platform(AbstractEntity): """ This class represent a platform (support), for instance "Playstation" """ - expected_fields = { + expected_fields: dict[str, Any] = { 'name': {'field': 'name', 'method': '_name', 'required': True, 'type': 'text'}, } - authorized_extra_fields_for_filtering = { + authorized_extra_fields_for_filtering: dict[str, Any] = { 'id': {'field': 'id', 'origin': 'native', 'type': 'int'}, 'versionCount': {'field': 'versionCount', 'origin': 'computed', 'type': 'int'} } @@ -15,31 +18,29 @@ class Platform(AbstractEntity): table_name = 'platforms' primary_key = 'id' - def __init__(self, entity_id, name, version_count = None,): + def __init__(self, entity_id: int | None, name: str, version_count: int | None = None) -> None: self.entity_id = entity_id self.name = name self.version_count = int(version_count or 0) - def get_id(self): + def get_id(self) -> int | None: """Return the id of the platform, for instance "3".""" return self.entity_id - def get_name(self): + def get_name(self) -> str: """Return the name of the platform, for instance "Playstation 2".""" return self.name - def set_name(self, name): + def set_name(self, name: str) -> None: self.name = name - def get_version_count(self): + def get_version_count(self) -> int: return self.version_count - def set_version_count(self, version_count): + def set_version_count(self, version_count: int | None) -> None: self.version_count = int(version_count or 0) - def serialize(self): + def serialize(self) -> dict[str, Any]: values = super().serialize() - values['versionCount'] = self.get_version_count() - return values diff --git a/src/entity/story.py b/src/entity/story.py index 3dae528..4085280 100644 --- a/src/entity/story.py +++ b/src/entity/story.py @@ -1,9 +1,12 @@ +from typing import Any + from src.entity.abstract_entity import AbstractEntity + class Story(AbstractEntity): """ This class represent a history entry, for instance I watched or played this game in 2021 """ - expected_fields = { + expected_fields: dict[str, Any] = { 'versionId': { 'field': 'version_id', 'method': '_version_id', @@ -16,7 +19,7 @@ class Story(AbstractEntity): 'played': {'field': 'played', 'method': '_played', 'required': True, 'type': 'int'}, } - authorized_extra_fields_for_filtering = { + authorized_extra_fields_for_filtering: dict[str, Any] = { 'id': {'field': 'id', 'origin': 'native', 'type': 'int'}, 'platformName': {'field': 'platformName', 'origin': 'computed', 'type': 'string'}, 'gameTitle': {'field': 'gameTitle', 'origin': 'computed', 'type': 'string'}, @@ -27,15 +30,15 @@ class Story(AbstractEntity): def __init__( self, - entity_id, - version_id, - year, - position, - watched, - played, - platform_name = None, - game_title = None, - ): + entity_id: int | None, + version_id: int, + year: int, + position: int, + watched: int, + played: int, + platform_name: str | None = None, + game_title: str | None = None, + ) -> None: self.entity_id = entity_id self.version_id = version_id self.year = year @@ -45,55 +48,53 @@ def __init__( self.platform_name = platform_name self.game_title = game_title - def get_id(self): + def get_id(self) -> int | None: return self.entity_id - def get_version_id(self): + def get_version_id(self) -> int: return self.version_id - def get_year(self): + def get_year(self) -> int: return self.year - def get_position(self): + def get_position(self) -> int: return self.position - def get_watched(self): + def get_watched(self) -> bool: return self.watched - def get_played(self): + def get_played(self) -> bool: return self.played - def set_version_id(self, version_id): + def set_version_id(self, version_id: int) -> None: self.version_id = version_id - def set_year(self, year): + def set_year(self, year: int) -> None: self.year = year - def set_position(self, position): + def set_position(self, position: int) -> None: self.position = position - def set_watched(self, status): + def set_watched(self, status: int) -> None: self.watched = bool(status) - def set_played(self, status): - self.played= bool(status) + def set_played(self, status: int) -> None: + self.played = bool(status) - def get_game_title(self): + def get_game_title(self) -> str | None: return self.game_title - def set_game_title(self, title): + def set_game_title(self, title: str) -> None: self.game_title = title - def get_platform_name(self): + def get_platform_name(self) -> str | None: return self.platform_name - def set_platform_name(self, platform_name): + def set_platform_name(self, platform_name: str) -> None: self.platform_name = platform_name - def serialize(self): + def serialize(self) -> dict[str, Any]: values = super().serialize() - values['platformName'] = self.get_platform_name() values['gameTitle'] = self.get_game_title() - return values diff --git a/src/entity/transaction.py b/src/entity/transaction.py index d74378a..cef77fa 100644 --- a/src/entity/transaction.py +++ b/src/entity/transaction.py @@ -1,7 +1,12 @@ +from typing import Any + from src.entity.abstract_entity import AbstractEntity + class Transaction(AbstractEntity): - expected_fields = { + """ This class represent a transaction entry, for instance I sell or bought a game """ + + expected_fields: dict[str, Any] = { 'versionId': {'field': 'version_id', 'method': '_version_id', 'required': True, 'type': 'int'}, 'copyId': {'field': 'copy_id', 'method': '_copy_id', 'required': False, 'type': 'int'}, 'year': {'field': 'year', 'method': '_year', 'required': True, 'type': 'int'}, @@ -30,7 +35,7 @@ class Transaction(AbstractEntity): }, } - authorized_extra_fields_for_filtering = { + authorized_extra_fields_for_filtering: dict[str, Any] = { 'id': {'field': 'transaction_id', 'origin': 'native', 'type': 'int'}, 'platformName': {'field': 'platformName', 'origin': 'computed', 'type': 'string'}, 'gameTitle': {'field': 'gameTitle', 'origin': 'computed', 'type': 'string'}, @@ -39,23 +44,22 @@ class Transaction(AbstractEntity): table_name = 'transactions' primary_key = 'transaction_id' - transaction_in = {'Bought', 'Loan-out-return', 'Loan-in',} - transaction_out = {'Sold', 'Loan-out', 'Loan-in-return'} + transaction_in: set[str] = {'Bought', 'Loan-out-return', 'Loan-in'} + transaction_out: set[str] = {'Sold', 'Loan-out', 'Loan-in-return'} - """ This class represent a transaction entry, for instance I sell or bought a game """ def __init__( self, - entity_id, - version_id, - copy_id, - year, - month, - day, - type, - notes, - platform_name = None, - game_title = None, - ): + entity_id: int | None, + version_id: int, + copy_id: int | None, + year: int, + month: int, + day: int, + type: str, + notes: str, + platform_name: str | None = None, + game_title: str | None = None, + ) -> None: self.entity_id = entity_id self.version_id = version_id self.copy_id = copy_id @@ -67,67 +71,65 @@ def __init__( self.platform_name = platform_name self.game_title = game_title - def get_id(self): + def get_id(self) -> int | None: return self.entity_id - def get_version_id(self): + def get_version_id(self) -> int: return self.version_id - def get_copy_id(self): + def get_copy_id(self) -> int | None: return self.copy_id - def get_year(self): + def get_year(self) -> int: return self.year - def get_month(self): + def get_month(self) -> int: return self.month - def get_day(self): + def get_day(self) -> int: return self.day - def get_type(self): + def get_type(self) -> str: return self.type - def get_notes(self): + def get_notes(self) -> str: return self.notes - def set_version_id(self, version_id): + def set_version_id(self, version_id: int) -> None: self.version_id = version_id - def set_copy_id(self, entity_id): + def set_copy_id(self, entity_id: int | None) -> None: self.copy_id = entity_id - def set_year(self, year): + def set_year(self, year: int) -> None: self.year = year - def set_month(self, month): + def set_month(self, month: int) -> None: self.month = month - def set_day(self, day): + def set_day(self, day: int) -> None: self.day = day - def set_type(self, type): + def set_type(self, type: str) -> None: self.type = type - def set_notes(self, notes): + def set_notes(self, notes: str) -> None: self.notes = notes - def get_game_title(self): + def get_game_title(self) -> str | None: return self.game_title - def set_game_title(self, title): + def set_game_title(self, title: str) -> None: self.game_title = title - def get_platform_name(self): + def get_platform_name(self) -> str | None: return self.platform_name - def set_platform_name(self, platform_name): + def set_platform_name(self, platform_name: str) -> None: self.platform_name = platform_name - def serialize(self): + def serialize(self) -> dict[str, Any]: values = super().serialize() - values['platformName'] = self.get_platform_name() values['gameTitle'] = self.get_game_title() - return values diff --git a/src/entity/user.py b/src/entity/user.py index 2223247..70a576d 100644 --- a/src/entity/user.py +++ b/src/entity/user.py @@ -1,4 +1,5 @@ """ User entity for the GMG project """ +from typing import Any # NOTE: THIS ENTITY WORKS SLIGHTLY DIFFERENTLY THAN THE OTHERS, FOR SECURITY MATTERS. # FOR INSTANCE, IT DOES NOT INHERIT FROM THE AbstractEntity, OR ITS REPOSITORY @@ -6,7 +7,7 @@ # WE DON'T WANT TO EXPOSE SENSITIVE VALUES class User: """ This class represent a user""" - expected_fields = { + expected_fields: dict[str, Any] = { 'email': {'field': 'email', 'method': '_email', 'required': True, 'type': 'text'}, 'password': {'field': 'password', 'method': '_password', 'required': True, 'type': 'text'}, 'active': { @@ -24,12 +25,12 @@ class User: }, } - authorized_extra_fields_for_filtering = {} + authorized_extra_fields_for_filtering: dict[str, Any] = {} table_name = 'users' primary_key = 'id' - def __init__(self, entity_id, email, password, status, user_name, salt, token): + def __init__(self, entity_id: int | None, email: str, password: str, status: int, user_name: str, salt: str | None, token: str | None) -> None: self.user_id = entity_id self.email = email self.password = password @@ -38,52 +39,48 @@ def __init__(self, entity_id, email, password, status, user_name, salt, token): self.salt = salt self.token = token - def get_id(self): + def get_id(self) -> int | None: """Return the id of the user, for instance "125".""" - return self.user_id - def get_email(self): + def get_email(self) -> str: """Return the email of the user.""" - return self.email - def get_salt(self): + def get_salt(self) -> str | None: """Return the salt of the user's password.""" - return self.salt - def get_password(self): + def get_password(self) -> str: """Return the user's password.""" - return self.password - def get_is_active(self): + def get_is_active(self) -> bool: """Return the user's status.""" return self.status == 1 - def get_user_name(self): + def get_user_name(self) -> str: """Returns the username""" return self.user_name - def get_token(self): + def get_token(self) -> str | None: """Returns the user's API token""" return self.token - def set_token(self, new_token): + def set_token(self, new_token: str) -> None: self.token = new_token - def set_salt(self, salt): + def set_salt(self, salt: str) -> None: self.salt = salt - def set_is_active(self, new_status): + def set_is_active(self, new_status: int) -> None: self.status = int(new_status) - def set_email(self, new_email): + def set_email(self, new_email: str) -> None: self.email = new_email - def set_user_name(self, new_user_name): + def set_user_name(self, new_user_name: str) -> None: self.user_name = new_user_name - def set_password(self, new_password): + def set_password(self, new_password: str) -> None: self.password = new_password diff --git a/src/entity/version.py b/src/entity/version.py index c714859..0104f73 100644 --- a/src/entity/version.py +++ b/src/entity/version.py @@ -1,9 +1,12 @@ +from typing import Any + from src.entity.abstract_entity import AbstractEntity + class Version(AbstractEntity): """ This class represent a version of a game, e.g the PC version of Monkey Island IV """ # If you change the order here, you need to also change it in the constructor! - expected_fields = { + expected_fields: dict[str, Any] = { 'platformId': {'field': 'platform_id', 'method': '_platform_id', @@ -156,7 +159,7 @@ class Version(AbstractEntity): }, } - authorized_extra_fields_for_filtering = { + authorized_extra_fields_for_filtering: dict[str, Any] = { 'id': {'field': 'version_id', 'origin': 'native', 'type': 'int'}, 'storyCount': {'field': 'storyCount', 'origin': 'computed', 'type': 'int'}, 'copyCount': {'field': 'copyCount', 'origin': 'computed', 'type': 'int'}, @@ -168,38 +171,38 @@ class Version(AbstractEntity): primary_key = 'version_id' # If you change the order here, you need to also change it in the array above! - def __init__( + def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments self, - entity_id, - platform_id, - game_id, - release_year, - todo_solo_sometimes, - todo_multiplayer_sometimes, - singleplayer_recurring, - multiplayer_recurring, - to_do, - to_buy, - to_watch_background, - to_watch_serious, - to_rewatch, - top_game, - hall_of_fame, - hall_of_fame_year, - hall_of_fame_position, - played_it_often, - ongoing, - comments, - todo_with_help, - bgf, - to_watch_position, - to_do_position, - finished, - platform_name = None, - game_title = None, - story_count = None, - copy_count = None, - ): + entity_id: int | None, + platform_id: int, + game_id: int, + release_year: int, + todo_solo_sometimes: int, + todo_multiplayer_sometimes: int, + singleplayer_recurring: int, + multiplayer_recurring: int, + to_do: int, + to_buy: int, + to_watch_background: int, + to_watch_serious: int, + to_rewatch: int, + top_game: int, + hall_of_fame: int, + hall_of_fame_year: int, + hall_of_fame_position: int, + played_it_often: int, + ongoing: int, + comments: str, + todo_with_help: int, + bgf: int, + to_watch_position: int, + to_do_position: int, + finished: int, + platform_name: str | None = None, + game_title: str | None = None, + story_count: int | None = None, + copy_count: int | None = None, + ) -> None: self.entity_id = entity_id self.platform_id = int(platform_id) self.game_id = int(game_id) @@ -230,183 +233,181 @@ def __init__( self.story_count = story_count self.copy_count = copy_count - def get_id(self): + def get_id(self) -> int | None: return self.entity_id - def get_platform_id(self): + def get_platform_id(self) -> int: return self.platform_id - def set_platform_id(self, entity_id): + def set_platform_id(self, entity_id: int) -> None: self.platform_id = int(entity_id) - def get_game_id(self): + def get_game_id(self) -> int: return self.game_id - def set_game_id(self, entity_id): + def set_game_id(self, entity_id: int) -> None: self.game_id = int(entity_id) - def get_release_year(self): + def get_release_year(self) -> int: return self.release_year - def set_release_year(self, year): + def set_release_year(self, year: int) -> None: self.release_year = int(year) - def get_todo_solo_sometimes(self): + def get_todo_solo_sometimes(self) -> bool: return self.todo_solo_sometimes - def set_todo_solo_sometimes(self, status): + def set_todo_solo_sometimes(self, status: int) -> None: self.todo_solo_sometimes = bool(status) - def get_todo_multiplayer_sometimes(self): + def get_todo_multiplayer_sometimes(self) -> bool: return self.todo_multiplayer_sometimes - def set_todo_multiplayer_sometimes(self, status): + def set_todo_multiplayer_sometimes(self, status: int) -> None: self.todo_multiplayer_sometimes = bool(status) - def get_singleplayer_recurring(self): + def get_singleplayer_recurring(self) -> bool: return self.singleplayer_recurring - def set_singleplayer_recurring(self, status): + def set_singleplayer_recurring(self, status: int) -> None: self.singleplayer_recurring = bool(status) - def get_multiplayer_recurring(self): + def get_multiplayer_recurring(self) -> bool: return self.multiplayer_recurring - def set_multiplayer_recurring(self, status): + def set_multiplayer_recurring(self, status: int) -> None: self.multiplayer_recurring = bool(status) - def get_to_do(self): + def get_to_do(self) -> bool: return self.to_do - def set_to_do(self, status): + def set_to_do(self, status: int) -> None: self.to_do = bool(status) - def get_to_buy(self): + def get_to_buy(self) -> bool: return self.to_buy - def set_to_buy(self, status): + def set_to_buy(self, status: int) -> None: self.to_buy = bool(status) - def get_to_watch_background(self): + def get_to_watch_background(self) -> bool: return self.to_watch_background - def set_to_watch_background(self, status): + def set_to_watch_background(self, status: int) -> None: self.to_watch_background = bool(status) - def get_to_watch_serious(self): + def get_to_watch_serious(self) -> bool: return self.to_watch_serious - def set_to_watch_serious(self, status): + def set_to_watch_serious(self, status: int) -> None: self.to_watch_serious = bool(status) - def get_to_rewatch(self): + def get_to_rewatch(self) -> bool: return self.to_rewatch - def set_to_rewatch(self, status): + def set_to_rewatch(self, status: int) -> None: self.to_rewatch = bool(status) - def get_top_game(self): + def get_top_game(self) -> bool: return self.top_game - def set_top_game(self, status): + def set_top_game(self, status: int) -> None: self.top_game = bool(status) - def get_hall_of_fame(self): + def get_hall_of_fame(self) -> bool: return self.hall_of_fame - def set_hall_of_fame(self, status): + def set_hall_of_fame(self, status: int) -> None: self.hall_of_fame = bool(status) - def get_hall_of_fame_year(self): + def get_hall_of_fame_year(self) -> int: return self.hall_of_fame_year - def set_hall_of_fame_year(self, year): + def set_hall_of_fame_year(self, year: int) -> None: self.hall_of_fame_year = int(year) - def get_hall_of_fame_position(self): + def get_hall_of_fame_position(self) -> int: return self.hall_of_fame_position - def set_hall_of_fame_position(self, position): + def set_hall_of_fame_position(self, position: int) -> None: self.hall_of_fame_position = int(position) - def get_played_it_often(self): + def get_played_it_often(self) -> bool: return self.played_it_often - def set_played_it_often(self, status): + def set_played_it_often(self, status: int) -> None: self.played_it_often = bool(status) - def get_ongoing(self): + def get_ongoing(self) -> bool: return self.ongoing - def set_ongoing(self, status): + def set_ongoing(self, status: int) -> None: self.ongoing = bool(status) - def get_comments(self): + def get_comments(self) -> str: return self.comments - def set_comments(self, comments): + def set_comments(self, comments: str) -> None: self.comments = comments - def get_todo_with_help(self): + def get_todo_with_help(self) -> bool: return self.todo_with_help - def set_todo_with_help(self, status): + def set_todo_with_help(self, status: int) -> None: self.todo_with_help = bool(status) - def get_best_game_forever(self): + def get_best_game_forever(self) -> bool: return self.bgf - def set_best_game_forever(self, status): + def set_best_game_forever(self, status: int) -> None: self.bgf = bool(status) - def get_to_watch_position(self): + def get_to_watch_position(self) -> int: return self.to_watch_position - def set_to_watch_position(self, position): + def set_to_watch_position(self, position: int) -> None: self.to_watch_position = int(position) - def get_to_do_position(self): + def get_to_do_position(self) -> int: return self.to_do_position - def set_to_do_position(self, position): + def set_to_do_position(self, position: int) -> None: self.to_do_position = int(position) - def get_platform_name(self): + def get_platform_name(self) -> str | None: return self.platform_name - def set_platform_name(self, name): + def set_platform_name(self, name: str) -> None: self.platform_name = name - def get_game_title(self): + def get_game_title(self) -> str | None: return self.game_title - def set_game_title(self, title): + def set_game_title(self, title: str) -> None: self.game_title = title - def get_finished(self): + def get_finished(self) -> bool: return self.finished - def set_finished(self, finished): + def set_finished(self, finished: int) -> None: self.finished = bool(finished) - def get_story_count(self): + def get_story_count(self) -> int | None: return self.story_count - def set_story_count(self, story_count): + def set_story_count(self, story_count: int | None) -> None: self.story_count = int(story_count or 0) - def get_copy_count(self): + def get_copy_count(self) -> int | None: return self.copy_count - def set_copy_count(self, copy_count): + def set_copy_count(self, copy_count: int | None) -> None: self.copy_count = int(copy_count or 0) - def serialize(self): + def serialize(self) -> dict[str, Any]: values = super().serialize() - values['platformName'] = self.get_platform_name() values['gameTitle'] = self.get_game_title() values['storyCount'] = self.get_story_count() values['copyCount'] = self.get_copy_count() - return values diff --git a/src/exception/duplicate_consecutive_operation.py b/src/exception/duplicate_consecutive_operation.py index d8bd430..57b513b 100644 --- a/src/exception/duplicate_consecutive_operation.py +++ b/src/exception/duplicate_consecutive_operation.py @@ -2,11 +2,11 @@ class DuplicateConsecutiveOperation(InconsistentOperation): """Raised when you try, for instance, to create two consecutive inbound transaction""" - def __init__(self, transaction_type): + def __init__(self, transaction_type: str) -> None: msg = 'Inconsistent transaction. You tried to create a transaction an ' msg += f"{transaction_type} transaction while the last registered " msg += 'transaction for this copy has of the same kind!' super().__init__(msg) - def get_code(self): + def get_code(self) -> int: return 15 diff --git a/src/exception/inactive_user_exception.py b/src/exception/inactive_user_exception.py index 8e7ccee..ffa1f9a 100644 --- a/src/exception/inactive_user_exception.py +++ b/src/exception/inactive_user_exception.py @@ -1,6 +1,6 @@ class InactiveUserException(Exception): - def __init__(self, field, value): + def __init__(self, field: str, value: str) -> None: super().__init__(f"The user with {field} = {value} is inactive.") - def get_code(self): + def get_code(self) -> int: return 3 diff --git a/src/exception/inconsistent_operation.py b/src/exception/inconsistent_operation.py index 30998c9..6a325cd 100644 --- a/src/exception/inconsistent_operation.py +++ b/src/exception/inconsistent_operation.py @@ -1,2 +1,2 @@ class InconsistentOperation(Exception): - "Abstract" \ No newline at end of file + """Abstract base for inconsistent operation exceptions""" diff --git a/src/exception/inconsistent_transaction_type_operation.py b/src/exception/inconsistent_transaction_type_operation.py index 6559eb2..04c5674 100644 --- a/src/exception/inconsistent_transaction_type_operation.py +++ b/src/exception/inconsistent_transaction_type_operation.py @@ -2,10 +2,10 @@ class InconsistentTransactionTypeOperation(InconsistentOperation): """Raised when you try, for instance, to sold a copy you don't have""" - def __init__(self, transaction_type, current_copy_status): + def __init__(self, transaction_type: str, current_copy_status: str) -> None: msg = 'Inconsistent transaction. You tried to create a transaction of ' msg += f"type '{transaction_type}' while the copy status is '{current_copy_status}'." super().__init__(msg) - def get_code(self): + def get_code(self) -> int: return 4 diff --git a/src/exception/inconsistent_version_and_copy_id.py b/src/exception/inconsistent_version_and_copy_id.py index 974bbe7..be86964 100644 --- a/src/exception/inconsistent_version_and_copy_id.py +++ b/src/exception/inconsistent_version_and_copy_id.py @@ -2,10 +2,10 @@ class InconsistentVersionAndCopyIdException(InconsistentOperation): """Raised when you try to create a transaction for which version_id and the copy version_id don't match.""" - def __init__(self, transaction_version_id, copy_version_id): + def __init__(self, transaction_version_id: int | None, copy_version_id: int | None) -> None: msg = 'Inconsistent transaction. You tried to create a transaction with versionId ' msg += f"= '{transaction_version_id}' while the copy versionId is '{copy_version_id}'." super().__init__(msg) - def get_code(self): + def get_code(self) -> int: return 14 diff --git a/src/exception/invalid_credentials_exception.py b/src/exception/invalid_credentials_exception.py index 8f26104..c7dcc58 100644 --- a/src/exception/invalid_credentials_exception.py +++ b/src/exception/invalid_credentials_exception.py @@ -1,6 +1,6 @@ class InvalidCredentialsException(Exception): - def __init__(self): + def __init__(self) -> None: super().__init__("The credentials are invalid.") - def get_code(self): + def get_code(self) -> int: return 2 diff --git a/src/exception/invalid_input.py b/src/exception/invalid_input.py index 7c1d062..7047b2c 100644 --- a/src/exception/invalid_input.py +++ b/src/exception/invalid_input.py @@ -1,5 +1,5 @@ class InvalidInput(Exception): """Raised when the expected value is not good, inconsistent...""" - def get_code(self): + def get_code(self) -> int: return 5 diff --git a/src/exception/missing_field_exception.py b/src/exception/missing_field_exception.py index 345c6be..5fbaae7 100644 --- a/src/exception/missing_field_exception.py +++ b/src/exception/missing_field_exception.py @@ -1,7 +1,7 @@ class MissingFieldException(Exception): """Raised when the expected field is not found""" - def __init__(self, field): + def __init__(self, field: str) -> None: super().__init__(f"The following field is missing: {field}.") - def get_code(self): + def get_code(self) -> int: return 6 diff --git a/src/exception/missing_header_exception.py b/src/exception/missing_header_exception.py index 1308154..8938848 100644 --- a/src/exception/missing_header_exception.py +++ b/src/exception/missing_header_exception.py @@ -1,7 +1,7 @@ class MissingHeaderException(Exception): """Raised when the expected header is not found""" - def __init__(self, field): + def __init__(self, field: str) -> None: super().__init__(f"The following header is missing: {field}.") - def get_code(self): + def get_code(self) -> int: return 7 diff --git a/src/exception/resource_already_exists_exception.py b/src/exception/resource_already_exists_exception.py index 84f9677..c66831d 100644 --- a/src/exception/resource_already_exists_exception.py +++ b/src/exception/resource_already_exists_exception.py @@ -1,12 +1,12 @@ class ResourceAlreadyExistsException(Exception): """Raised when the expected resource already exists""" - def __init__(self, type, id, key = 'id'): + def __init__(self, type: str, id: str, key: str = 'id') -> None: if key == 'id': id = '#' + id - else : + else: id = "'" + id + "'" super().__init__(f"The resource of type '{type}' with {key} {id} already exists.") - def get_code(self): + def get_code(self) -> int: return 8 diff --git a/src/exception/resource_has_children_exception.py b/src/exception/resource_has_children_exception.py index a802d3b..bd03450 100644 --- a/src/exception/resource_has_children_exception.py +++ b/src/exception/resource_has_children_exception.py @@ -1,7 +1,7 @@ class RessourceHasChildrenException(Exception): """Raised when the resource has children, for instance a version of a game has stories""" - def __init__(self, resource_type, child_type): + def __init__(self, resource_type: str, child_type: str) -> None: super().__init__(f"The following resource type '{resource_type}' has children of type '{child_type}', so it cannot be deleted.") - def get_code(self): + def get_code(self) -> int: return 9 diff --git a/src/exception/unknown_resource_exception.py b/src/exception/unknown_resource_exception.py index 9937348..92533de 100644 --- a/src/exception/unknown_resource_exception.py +++ b/src/exception/unknown_resource_exception.py @@ -1,12 +1,12 @@ class ResourceNotFoundException(Exception): """Raised when the expected resource is not found""" - def __init__(self, type, id, key = 'id'): + def __init__(self, type: str, id: int | str, key: str = 'id') -> None: if key == 'id': id = '#' + str(id) - else : - id = "'" + id + "'" + else: + id = "'" + str(id) + "'" super().__init__(f"The resource of type '{type}' with {key} {id} has not been found.") - def get_code(self): + def get_code(self) -> int: return 1 diff --git a/src/exception/unsupported_filter_exception.py b/src/exception/unsupported_filter_exception.py index 6619824..f05b7af 100644 --- a/src/exception/unsupported_filter_exception.py +++ b/src/exception/unsupported_filter_exception.py @@ -1,6 +1,6 @@ class UnsupportedFilterException(Exception): - def __init__(self, field, allowed_filters): + def __init__(self, field: str, allowed_filters: list[str]) -> None: super().__init__(f"The following filter is not allowed: {field}. Allowed filters are: " + ', '.join(allowed_filters) + '.') - def get_code(self): + def get_code(self) -> int: return 10 diff --git a/src/exception/unsupported_value_exception.py b/src/exception/unsupported_value_exception.py index 36d086d..ccc179d 100644 --- a/src/exception/unsupported_value_exception.py +++ b/src/exception/unsupported_value_exception.py @@ -1,6 +1,6 @@ class UnsupportedValueException(Exception): - def __init__(self, field, value, supported_values): + def __init__(self, field: str, value: str, supported_values: set[str]) -> None: super().__init__(f"The field '{field}' does not support the value '{value}'. Supported values are: " + ', '.join(sorted(supported_values)) + '.') - def get_code(self): + def get_code(self) -> int: return 11 diff --git a/src/helpers/json_helper.py b/src/helpers/json_helper.py index 2ae83e2..79b8236 100644 --- a/src/helpers/json_helper.py +++ b/src/helpers/json_helper.py @@ -1,8 +1,11 @@ +from typing import Any + from flask import request -class JsonHelper: # pylint: disable=too-few-public-methods + +class JsonHelper: # pylint: disable=too-few-public-methods @classmethod - def get_value_from_request(cls, key, default = None): + def get_value_from_request(cls, key: str, default: Any = None) -> Any: data = request.get_json() if key in data: diff --git a/src/repository/abstract_core_repository.py b/src/repository/abstract_core_repository.py index f6fe518..4408305 100644 --- a/src/repository/abstract_core_repository.py +++ b/src/repository/abstract_core_repository.py @@ -1,10 +1,12 @@ +from typing import Any -class AbstractCoreRepository: # pylint: disable=no-member + +class AbstractCoreRepository: # pylint: disable=no-member """Another useless comment""" - def __init__(self, mysql): + def __init__(self, mysql: Any) -> None: self.mysql = mysql - def fetch_one(self, request, data_tuple): + def fetch_one(self, request: str, data_tuple: tuple[Any, ...]) -> Any: """Fetch one result from a given request.""" cursor = self.mysql.cursor(dictionary=True) cursor.execute(request, data_tuple) @@ -18,7 +20,7 @@ def fetch_one(self, request, data_tuple): return hydrated - def fetch_multiple(self, request, data_tuple): + def fetch_multiple(self, request: str, data_tuple: tuple[Any, ...]) -> list[Any]: """Fetch mutliple items and return a list.""" items_list = [] cursor = self.mysql.cursor(dictionary=True, buffered=True) @@ -33,19 +35,19 @@ def fetch_multiple(self, request, data_tuple): cursor.close() return items_list - def hydrate(self, row): + def hydrate(self, row: dict[str, Any]) -> Any: """Hydrate an object from a row.""" values = [] values.append(row[self.entity.primary_key]) - for api_field, data in self.entity.expected_fields.items():# pylint: disable=W0612 + for api_field, data in self.entity.expected_fields.items(): # pylint: disable=W0612 values.append(row[data['field']]) object = self.entity(*values) return object - def write(self, request, data, commit=True): + def write(self, request: str, data: list[Any] | tuple[Any, ...], commit: bool = True) -> int | None: """Performs an UPDATE or WRITE statement""" cursor = self.mysql.cursor() cursor.execute(request, data) @@ -57,7 +59,7 @@ def write(self, request, data, commit=True): return cursor.lastrowid - def fetch_cursor(self, request, data_tuple = None): + def fetch_cursor(self, request: str, data_tuple: list[Any] | dict[str, Any] | None = None) -> dict[str, Any] | None: """Fetch one result""" if data_tuple is None: data_tuple = {} diff --git a/src/repository/abstract_repository.py b/src/repository/abstract_repository.py index 95ad455..9fbf4b5 100644 --- a/src/repository/abstract_repository.py +++ b/src/repository/abstract_repository.py @@ -1,29 +1,34 @@ """An abstract repository""" import math +from typing import Any + +from werkzeug.datastructures import ImmutableMultiDict + from src.repository.abstract_core_repository import AbstractCoreRepository + class AbstractRepository(AbstractCoreRepository): - def get_by_id(self, entity_id): + def get_by_id(self, entity_id: int) -> Any: """Get one support by its primary key.""" request = self.get_select_request_start() - request += f" AND {self.entity.table_name}.{self.entity.primary_key} = %s LIMIT 1;" # pylint: disable=E1101 + request += f" AND {self.entity.table_name}.{self.entity.primary_key} = %s LIMIT 1;" # pylint: disable=E1101 return self.fetch_one(request, (entity_id,)) - def delete(self, entity_id, commit = True): - request = f"DELETE FROM {self.entity.table_name} WHERE {self.entity.primary_key} = %s" # pylint: disable=E1101 + def delete(self, entity_id: int, commit: bool = True) -> None: + request = f"DELETE FROM {self.entity.table_name} WHERE {self.entity.primary_key} = %s" # pylint: disable=E1101 self.write(request, (entity_id,), commit) - def get_select_request_start(self): - return f"SELECT * FROM {self.entity.table_name} WHERE {self.entity.primary_key} IS NOT NULL " # pylint: disable=E1101 + def get_select_request_start(self) -> str: + return f"SELECT * FROM {self.entity.table_name} WHERE {self.entity.primary_key} IS NOT NULL " # pylint: disable=E1101 - def get_list(self, filters, page, limit): + def get_list(self, filters: ImmutableMultiDict, page: int | str, limit: int | str) -> dict[str, Any]: filter_request = '' - values = [] + values: list[Any] = [] usable_fields = { - **self.entity.expected_fields, # pylint: disable=E1101 - **self.entity.authorized_extra_fields_for_filtering # pylint: disable=E1101 + **self.entity.expected_fields, # pylint: disable=E1101 + **self.entity.authorized_extra_fields_for_filtering # pylint: disable=E1101 } # Create the filter part of the SQL request @@ -38,9 +43,9 @@ def get_list(self, filters, page, limit): # Count the total of result without pagination count_request = "SELECT count(*) as count " - count_request += f"FROM ({self.get_select_request_start()}) AS e, {self.entity.table_name} " # pylint: disable=E1101 - count_request += f"WHERE {self.entity.table_name}.{self.entity.primary_key} IS NOT NULL " # pylint: disable=E1101 - count_request += f"AND {self.entity.table_name}.{self.entity.primary_key} = e.{self.entity.primary_key} " # pylint: disable=E1101 + count_request += f"FROM ({self.get_select_request_start()}) AS e, {self.entity.table_name} " # pylint: disable=E1101 + count_request += f"WHERE {self.entity.table_name}.{self.entity.primary_key} IS NOT NULL " # pylint: disable=E1101 + count_request += f"AND {self.entity.table_name}.{self.entity.primary_key} = e.{self.entity.primary_key} " # pylint: disable=E1101 count_request += filter_request total_result_count = self.fetch_cursor(count_request, values)['count'] @@ -70,7 +75,7 @@ def get_list(self, filters, page, limit): "result": [entry.serialize() for entry in result] } - def get_order_by_conditions(self, usable_fields, filters): + def get_order_by_conditions(self, usable_fields: dict[str, Any], filters: ImmutableMultiDict) -> str: filter_request = '' order_by_filters = filters.getlist('orderBy[]') @@ -89,7 +94,7 @@ def get_order_by_conditions(self, usable_fields, filters): filter_request += 'RAND(), ' if filter_request == '': - filter_request = 'ORDER BY ' + self.entity.primary_key + ' ASC ' # pylint: disable=E1101 + filter_request = 'ORDER BY ' + self.entity.primary_key + ' ASC ' # pylint: disable=E1101 else: length = len(filter_request) filter_request = 'ORDER BY ' + filter_request[:length-2] + ' ' @@ -98,7 +103,7 @@ def get_order_by_conditions(self, usable_fields, filters): # This method handles the creation of the SQL conditions for each filter @classmethod - def create_get_list_filter_condition(cls, current_filter_values, filter_data, field, values): + def create_get_list_filter_condition(cls, current_filter_values: list[str], filter_data: dict[str, Any], field: str, values: list[Any]) -> str: comparison_operators = { 'lt': '<', 'gt': '>', @@ -110,11 +115,11 @@ def create_get_list_filter_condition(cls, current_filter_values, filter_data, fi # Loop on all the values given for this filter for filter_value in current_filter_values: # Various possibility according to the field type - if filter_data['type'] == 'int' or filter_data['type']== 'strict-text': + if filter_data['type'] == 'int' or filter_data['type'] == 'strict-text': comparison_operator = ' = ' if filter_data['type'] == 'int': - for comp_url_key, comp_sql_key in comparison_operators.items():# pylint: disable=W0612 + for comp_url_key, comp_sql_key in comparison_operators.items(): # pylint: disable=W0612 if filter_value.startswith(comp_url_key + '-'): array = filter_value.split('-') comparison_operator = comparison_operators[array[0]] @@ -122,7 +127,7 @@ def create_get_list_filter_condition(cls, current_filter_values, filter_data, fi or_request += field + f" {comparison_operator} %s OR " value_to_bind = filter_value - else: # text + else: # text or_request += field + " LIKE %s OR " value_to_bind = f"%{filter_value}%" @@ -134,18 +139,18 @@ def create_get_list_filter_condition(cls, current_filter_values, filter_data, fi return or_request - def insert(self, object, commit = True): + def insert(self, object: Any, commit: bool = True) -> Any: """Insert a new entry""" request = f"INSERT INTO {object.table_name} (" - for api_field, data in object.expected_fields.items(): # pylint: disable=W0612 + for api_field, data in object.expected_fields.items(): # pylint: disable=W0612 request += data['field'] + ', ' length = len(request) request = request[:length-2] request += ') VALUES (' - for api_field, data in object.expected_fields.items(): # pylint: disable=W0612 + for api_field, data in object.expected_fields.items(): # pylint: disable=W0612 request += '%s, ' length = len(request) @@ -153,16 +158,16 @@ def insert(self, object, commit = True): request += ')' values = [] - for api_field, data in object.expected_fields.items(): # pylint: disable=W0612 + for api_field, data in object.expected_fields.items(): # pylint: disable=W0612 method_to_call = getattr(object, 'get' + data['method']) values.append(method_to_call()) return self.get_by_id(self.write(request, values, commit)) - def update(self, object, commit = True): + def update(self, object: Any, commit: bool = True) -> Any: request = f"UPDATE {object.table_name} SET " - for api_field, data in object.expected_fields.items(): # pylint: disable=W0612 + for api_field, data in object.expected_fields.items(): # pylint: disable=W0612 request += data['field'] + ' = %s, ' length = len(request) diff --git a/src/repository/copy_repository.py b/src/repository/copy_repository.py index 215e3c6..85fe98d 100644 --- a/src/repository/copy_repository.py +++ b/src/repository/copy_repository.py @@ -1,3 +1,5 @@ +from typing import Any + from src.repository.abstract_repository import AbstractRepository from src.entity.copy import Copy from src.entity.transaction import Transaction @@ -5,10 +7,11 @@ from src.entity.game import Game from src.entity.platform import Platform + class CopyRepository(AbstractRepository): entity = Copy - def get_select_request_start(self): + def get_select_request_start(self) -> str: request = f"SELECT {Copy.table_name}.*, v.transactionCount AS transactionCount, " request += f"{Game.table_name}.title AS gameTitle, {Platform.table_name}.name AS platformName " request += 'FROM ' @@ -25,7 +28,7 @@ def get_select_request_start(self): return request - def hydrate(self, row): + def hydrate(self, row: dict[str, Any]) -> Copy: """Hydrate an object from a row.""" copy = super().hydrate(row) copy.set_platform_name(row['platformName']) diff --git a/src/repository/game_repository.py b/src/repository/game_repository.py index 3b8689c..6ae03d1 100644 --- a/src/repository/game_repository.py +++ b/src/repository/game_repository.py @@ -1,12 +1,15 @@ """ Repository to handle the games """ +from typing import Any + from src.repository.abstract_repository import AbstractRepository from src.entity.game import Game from src.entity.version import Version + class GameRepository(AbstractRepository): entity = Game - def get_select_request_start(self): + def get_select_request_start(self) -> str: request = f"SELECT {Game.table_name}.*, v.versionCount AS versionCount " request += 'FROM ' request += f" (SELECT COUNT(*) AS versionCount, {Game.table_name}.id AS game_id " @@ -18,13 +21,13 @@ def get_select_request_start(self): return request - def get_by_title(self, title): + def get_by_title(self, title: str) -> Game | None: """Get one support by its title.""" request = self.get_select_request_start() + f"AND {Game.table_name}.title = %s LIMIT 1;" return self.fetch_one(request, (title,)) - def hydrate(self, row): + def hydrate(self, row: dict[str, Any]) -> Game: """Hydrate an object from a row.""" version = super().hydrate(row) version.set_version_count(row['versionCount']) diff --git a/src/repository/note_repository.py b/src/repository/note_repository.py index edaf6c1..cf71bb1 100644 --- a/src/repository/note_repository.py +++ b/src/repository/note_repository.py @@ -1,12 +1,13 @@ """ Repository to handle the notes """ +from typing import Any + from src.repository.abstract_repository import AbstractRepository from src.entity.note import Note + class NoteRepository(AbstractRepository): entity = Note - def hydrate(self, row): + def hydrate(self, row: dict[str, Any]) -> Note: """Hydrate an object from a row.""" - version = super().hydrate(row) - - return version + return super().hydrate(row) diff --git a/src/repository/platform_repository.py b/src/repository/platform_repository.py index 332705c..a23cfa8 100644 --- a/src/repository/platform_repository.py +++ b/src/repository/platform_repository.py @@ -1,12 +1,15 @@ """ Repository to handle the platforms """ +from typing import Any + from src.repository.abstract_repository import AbstractRepository from src.entity.platform import Platform from src.entity.version import Version + class PlatformRepository(AbstractRepository): entity = Platform - def get_select_request_start(self): + def get_select_request_start(self) -> str: request = f"SELECT {Platform.table_name}.*, v.versionCount AS versionCount " request += 'FROM ' request += f" (SELECT COUNT(*) AS versionCount, {Platform.table_name}.id AS platform_id " @@ -18,13 +21,13 @@ def get_select_request_start(self): return request - def get_by_name(self, name): + def get_by_name(self, name: str) -> Platform | None: """Get one support by its name.""" request = self.get_select_request_start() + f"AND {Platform.table_name}.name = %s LIMIT 1;" return self.fetch_one(request, (name,)) - def hydrate(self, row): + def hydrate(self, row: dict[str, Any]) -> Platform: """Hydrate an object from a row.""" version = super().hydrate(row) version.set_version_count(row['versionCount']) diff --git a/src/repository/story_repository.py b/src/repository/story_repository.py index 95b2db8..d1b3642 100644 --- a/src/repository/story_repository.py +++ b/src/repository/story_repository.py @@ -1,13 +1,16 @@ +from typing import Any + from src.repository.abstract_repository import AbstractRepository from src.entity.story import Story from src.entity.version import Version from src.entity.game import Game from src.entity.platform import Platform + class StoryRepository(AbstractRepository): entity = Story - def get_select_request_start(self): + def get_select_request_start(self) -> str: request = f"SELECT {Story.table_name}.*, " request += f"{Game.table_name}.title AS gameTitle, " request += f"{Platform.table_name}.name AS platformName " @@ -18,7 +21,7 @@ def get_select_request_start(self): return request - def hydrate(self, row): + def hydrate(self, row: dict[str, Any]) -> Story: """Hydrate an object from a row.""" story = super().hydrate(row) story.set_platform_name(row['platformName']) diff --git a/src/repository/transaction_repository.py b/src/repository/transaction_repository.py index ad43744..a654c77 100644 --- a/src/repository/transaction_repository.py +++ b/src/repository/transaction_repository.py @@ -1,13 +1,16 @@ +from typing import Any + from src.repository.abstract_repository import AbstractRepository from src.entity.transaction import Transaction from src.entity.version import Version from src.entity.game import Game from src.entity.platform import Platform + class TransactionRepository(AbstractRepository): entity = Transaction - def get_select_request_start(self): + def get_select_request_start(self) -> str: request = f"SELECT {Transaction.table_name}.*, {Game.table_name}.title AS gameTitle, " request += f"{Platform.table_name}.name AS platformName " request += f"FROM {Transaction.table_name}, {Version.table_name}, {Game.table_name}, {Platform.table_name} " @@ -17,22 +20,22 @@ def get_select_request_start(self): return request - def get_count_by_version(self, version_id): + def get_count_by_version(self, version_id: int) -> int: return self.fetch_cursor( f"SELECT COUNT(*) AS count FROM {Transaction.table_name} WHERE version_id = %s", (version_id,) )['count'] - def reset_copy_id_for_transactions(self, copy_id, commit=True): + def reset_copy_id_for_transactions(self, copy_id: int, commit: bool = True) -> None: self.write(f"UPDATE {Transaction.table_name} SET copy_id = NULL WHERE copy_id = %s", (copy_id,), commit) - def get_last_transaction_for_copy(self, copy_id): + def get_last_transaction_for_copy(self, copy_id: int) -> Transaction | None: return self.fetch_one( f"{self.get_select_request_start()} AND copy_id = %s ORDER BY transaction_id DESC LIMIT 1", (copy_id,) ) - def hydrate(self, row): + def hydrate(self, row: dict[str, Any]) -> Transaction: """Hydrate an object from a row.""" version = super().hydrate(row) version.set_platform_name(row['platformName']) diff --git a/src/repository/user_repository.py b/src/repository/user_repository.py index 21b3038..ba0e970 100644 --- a/src/repository/user_repository.py +++ b/src/repository/user_repository.py @@ -1,30 +1,33 @@ """ Repository to handle the users """ +from typing import Any + from src.repository.abstract_repository import AbstractRepository from src.entity.user import User + class UserRepository(AbstractRepository): """ Another useless comment """ entity = User - def get_by_email(self, user_email): + def get_by_email(self, user_email: str) -> User | None: """Gets an user by email""" request = self.get_select_request_start() + "AND email = %s;" return self.fetch_one(request, (user_email,)) - def get_active_by_token(self, user_token): + def get_active_by_token(self, user_token: str) -> User | None: """Gets an user by its token""" request = self.get_select_request_start() + "AND token = %s AND status = 1 LIMIT 1;" return self.fetch_one(request, (user_token,)) - def get_by_user_name(self, user_name): + def get_by_user_name(self, user_name: str) -> User | None: """Check if a user already exists""" request = self.get_select_request_start() + "AND user_name = %s LIMIT 1;" return self.fetch_one(request, (user_name,)) - def insert(self, object, commit=True): + def insert(self, object: User, commit: bool = True) -> User | None: """Inserts an user""" request = "INSERT INTO users (email, password, salt, status, user_name, token)" request += " VALUES (%s, %s, %s, 0, %s, %s);" @@ -42,7 +45,7 @@ def insert(self, object, commit=True): return self.get_by_email(object.get_email()) - def update(self, object, commit=True): + def update(self, object: User, commit: bool = True) -> User | None: """Updates an user""" request = "UPDATE users SET email = %s, password = %s, status = %s, user_name = %s, token = %s " request += "WHERE id = %s;" @@ -61,7 +64,7 @@ def update(self, object, commit=True): return self.get_by_id(object.get_id()) - def hydrate(self, row): + def hydrate(self, row: dict[str, Any]) -> User: """Hydrate an object from a row.""" user = User( row['id'], diff --git a/src/repository/version_repository.py b/src/repository/version_repository.py index 9ef31f5..38f871a 100644 --- a/src/repository/version_repository.py +++ b/src/repository/version_repository.py @@ -1,4 +1,6 @@ """ Repository to handle the versions """ +from typing import Any + from src.repository.abstract_repository import AbstractRepository from src.entity.version import Version from src.entity.game import Game @@ -6,10 +8,11 @@ from src.entity.copy import Copy from src.entity.story import Story + class VersionRepository(AbstractRepository): entity = Version - def get_select_request_start(self): + def get_select_request_start(self) -> str: request = "SELECT versions.*, v.storyCount AS storyCount, c.copyCount AS copyCount, " request += f"{Game.table_name}.title AS gameTitle, {Platform.table_name}.name AS platformName " request += 'FROM ' @@ -33,7 +36,7 @@ def get_select_request_start(self): return request - def get_by_unique_index(self, platform_id, game_id): + def get_by_unique_index(self, platform_id: int, game_id: int) -> Version | None: """Get one version by the unique combination of the platform and the game.""" request = self.get_select_request_start() request += f"AND {Version.table_name}.platform_id = %s " @@ -41,7 +44,7 @@ def get_by_unique_index(self, platform_id, game_id): return self.fetch_one(request, (platform_id, game_id,)) - def hydrate(self, row): + def hydrate(self, row: dict[str, Any]) -> Version: """Hydrate an object from a row.""" version = super().hydrate(row) version.set_platform_name(row['platformName']) diff --git a/src/service/abstract_service.py b/src/service/abstract_service.py index 0dc4ef8..eb182bd 100644 --- a/src/service/abstract_service.py +++ b/src/service/abstract_service.py @@ -1,10 +1,13 @@ +from typing import Any + from src.exception.unknown_resource_exception import ResourceNotFoundException from src.exception.missing_field_exception import MissingFieldException from src.exception.unsupported_value_exception import UnsupportedValueException from src.helpers.json_helper import JsonHelper -class AbstractService: # pylint: disable=no-member - def get_by_id(self, entity_id): + +class AbstractService: # pylint: disable=no-member + def get_by_id(self, entity_id: int) -> Any: object = self.repository.get_by_id(entity_id) if object is None: @@ -12,14 +15,14 @@ def get_by_id(self, entity_id): return object - def insert(self, object): + def insert(self, object: Any) -> Any: return self.repository.insert(object) - def update(self, object): + def update(self, object: Any) -> Any: return self.repository.update(object) - def validate_payload_for_creation_and_hydrate(self, object): - values = [] + def validate_payload_for_creation_and_hydrate(self, object: type) -> Any: + values: list[Any] = [] values.append(None) for api_field, data in object.expected_fields.items(): @@ -42,7 +45,7 @@ def validate_payload_for_creation_and_hydrate(self, object): return object(*values) - def hydrate_for_update(self, object): + def hydrate_for_update(self, object: Any) -> None: for api_field, data in object.expected_fields.items(): value = JsonHelper.get_value_from_request(api_field, None) @@ -54,7 +57,7 @@ def hydrate_for_update(self, object): value = self.cast_type(data, value) method_to_call(value) - def cast_type(self, data, value): + def cast_type(self, data: dict[str, Any], value: Any) -> Any: if data['type'] == 'int': try: value = int(value) diff --git a/src/service/copy_service.py b/src/service/copy_service.py index ee82a25..cc927fa 100644 --- a/src/service/copy_service.py +++ b/src/service/copy_service.py @@ -1,3 +1,5 @@ +from typing import Any + from src.exception.resource_has_children_exception import RessourceHasChildrenException from src.service.abstract_service import AbstractService from src.repository.copy_repository import CopyRepository @@ -5,15 +7,15 @@ from src.entity.copy import Copy from src.exception.unknown_resource_exception import ResourceNotFoundException + class CopyService(AbstractService): resource_type = 'copy' - def __init__(self, mysql): + def __init__(self, mysql: Any) -> None: self.repository = CopyRepository(mysql) self.version_repository = VersionRepository(mysql) - def get_for_create(self): - + def get_for_create(self) -> Copy: copy = super().validate_payload_for_creation_and_hydrate(Copy) version = self.version_repository.get_by_id(copy.get_version_id()) @@ -22,7 +24,7 @@ def get_for_create(self): return copy - def get_for_update(self, copy_id): + def get_for_update(self, copy_id: int) -> Copy: copy = self.repository.get_by_id(copy_id) if copy is None: @@ -37,7 +39,7 @@ def get_for_update(self, copy_id): return copy - def delete(self, copy_id): + def delete(self, copy_id: int) -> bool: copy = self.repository.get_by_id(copy_id) if copy is None: diff --git a/src/service/game_service.py b/src/service/game_service.py index d17156b..66cb5e3 100644 --- a/src/service/game_service.py +++ b/src/service/game_service.py @@ -1,3 +1,5 @@ +from typing import Any + from src.service.abstract_service import AbstractService from src.repository.game_repository import GameRepository from src.entity.game import Game @@ -6,13 +8,14 @@ from src.exception.resource_has_children_exception import RessourceHasChildrenException from src.helpers.json_helper import JsonHelper + class GameService(AbstractService): resource_type = 'game' - def __init__(self, mysql): + def __init__(self, mysql: Any) -> None: self.repository = GameRepository(mysql) - def get_for_create(self): + def get_for_create(self) -> Game: game = super().validate_payload_for_creation_and_hydrate(Game) existing_version = self.repository.get_by_title(game.get_title()) @@ -22,7 +25,7 @@ def get_for_create(self): return game - def get_for_update(self, game_id): + def get_for_update(self, game_id: int) -> Game: # Verification game = self.repository.get_by_id(game_id) @@ -39,7 +42,7 @@ def get_for_update(self, game_id): return game - def delete(self, game_id): + def delete(self, game_id: int) -> bool: """Delete a game""" game = self.repository.get_by_id(game_id) diff --git a/src/service/note_service.py b/src/service/note_service.py index 2f90b47..eca1f2c 100644 --- a/src/service/note_service.py +++ b/src/service/note_service.py @@ -1,21 +1,21 @@ +from typing import Any + from src.service.abstract_service import AbstractService from src.repository.note_repository import NoteRepository from src.entity.note import Note from src.exception.unknown_resource_exception import ResourceNotFoundException + class NoteService(AbstractService): resource_type = 'note' - def __init__(self, mysql): + def __init__(self, mysql: Any) -> None: self.repository = NoteRepository(mysql) + def get_for_create(self) -> Note: + return super().validate_payload_for_creation_and_hydrate(Note) - def get_for_create(self): - note = super().validate_payload_for_creation_and_hydrate(Note) - - return note - - def get_for_update(self, note_id): + def get_for_update(self, note_id: int) -> Note: # Verification note = self.repository.get_by_id(note_id) @@ -26,7 +26,7 @@ def get_for_update(self, note_id): return note - def delete(self, note_id): + def delete(self, note_id: int) -> bool: note = self.repository.get_by_id(note_id) if note is None: diff --git a/src/service/platform_service.py b/src/service/platform_service.py index 2c22412..77dbb08 100644 --- a/src/service/platform_service.py +++ b/src/service/platform_service.py @@ -1,3 +1,5 @@ +from typing import Any + from src.service.abstract_service import AbstractService from src.repository.platform_repository import PlatformRepository from src.entity.platform import Platform @@ -6,13 +8,14 @@ from src.exception.resource_has_children_exception import RessourceHasChildrenException from src.helpers.json_helper import JsonHelper + class PlatformService(AbstractService): resource_type = 'platform' - def __init__(self, mysql): + def __init__(self, mysql: Any) -> None: self.repository = PlatformRepository(mysql) - def get_for_create(self): + def get_for_create(self) -> Platform: platform = super().validate_payload_for_creation_and_hydrate(Platform) existing_version = self.repository.get_by_name(platform.get_name()) @@ -22,7 +25,7 @@ def get_for_create(self): return platform - def get_for_update(self, platform_id): + def get_for_update(self, platform_id: int) -> Platform: # Verification platform = self.repository.get_by_id(platform_id) @@ -39,7 +42,7 @@ def get_for_update(self, platform_id): return platform - def delete(self, platform_id): + def delete(self, platform_id: int) -> bool: platform = self.repository.get_by_id(platform_id) if platform is None: diff --git a/src/service/story_service.py b/src/service/story_service.py index 1a24eee..4eee459 100644 --- a/src/service/story_service.py +++ b/src/service/story_service.py @@ -1,18 +1,20 @@ +from typing import Any + from src.service.abstract_service import AbstractService from src.repository.story_repository import StoryRepository from src.repository.version_repository import VersionRepository from src.entity.story import Story from src.exception.unknown_resource_exception import ResourceNotFoundException + class StoryService(AbstractService): resource_type = 'story' - def __init__(self, mysql): + def __init__(self, mysql: Any) -> None: self.repository = StoryRepository(mysql) self.version_repository = VersionRepository(mysql) - def get_for_create(self): - + def get_for_create(self) -> Story: story = super().validate_payload_for_creation_and_hydrate(Story) version = self.version_repository.get_by_id(story.get_version_id()) @@ -21,7 +23,7 @@ def get_for_create(self): return story - def get_for_update(self, story_id): + def get_for_update(self, story_id: int) -> Story: story = self.repository.get_by_id(story_id) if story is None: @@ -36,7 +38,7 @@ def get_for_update(self, story_id): return story - def delete(self, story_id): + def delete(self, story_id: int) -> bool: story = self.repository.get_by_id(story_id) if story is None: diff --git a/src/service/transaction_service.py b/src/service/transaction_service.py index 59b93c5..e1ac90f 100644 --- a/src/service/transaction_service.py +++ b/src/service/transaction_service.py @@ -1,3 +1,5 @@ +from typing import Any + from src.exception.duplicate_consecutive_operation import DuplicateConsecutiveOperation from src.exception.inconsistent_transaction_type_operation import InconsistentTransactionTypeOperation from src.exception.inconsistent_version_and_copy_id import InconsistentVersionAndCopyIdException @@ -6,20 +8,23 @@ from src.repository.copy_repository import CopyRepository from src.repository.version_repository import VersionRepository from src.entity.transaction import Transaction +from src.entity.copy import Copy +from src.entity.version import Version from src.exception.unknown_resource_exception import ResourceNotFoundException + class TransactionService(AbstractService): resource_type = 'transaction' - def __init__(self, mysql): + def __init__(self, mysql: Any) -> None: self.repository = TransactionRepository(mysql) self.copy_repository = CopyRepository(mysql) self.version_repository = VersionRepository(mysql) - def get_for_create(self): + def get_for_create(self) -> Transaction: return super().validate_payload_for_creation_and_hydrate(Transaction) - def get_for_update(self, transaction_id): + def get_for_update(self, transaction_id: int) -> Transaction: transaction = self.repository.get_by_id(transaction_id) if transaction is None: @@ -29,7 +34,7 @@ def get_for_update(self, transaction_id): return transaction - def insert(self, object): + def insert(self, object: Transaction) -> Transaction: version = self.get_version(object) copy = self.get_copy(object) self.check_no_duplicate_type_consecutive(copy, object) @@ -46,7 +51,7 @@ def insert(self, object): return object - def update(self, object): + def update(self, object: Transaction) -> Transaction: version = self.get_version(object) copy = self.get_copy(object) self.check_no_duplicate_type_consecutive(copy, object) @@ -63,7 +68,7 @@ def update(self, object): return object - def delete(self, transaction_id): + def delete(self, transaction_id: int) -> bool: transaction = self.repository.get_by_id(transaction_id) if transaction is None: @@ -73,7 +78,7 @@ def delete(self, transaction_id): return True - def get_version(self, transaction): + def get_version(self, transaction: Transaction) -> Version: version = self.version_repository.get_by_id(transaction.get_version_id()) if version is None: @@ -81,7 +86,7 @@ def get_version(self, transaction): return version - def get_copy(self, transaction): + def get_copy(self, transaction: Transaction) -> Copy | None: if transaction.get_copy_id() is not None: copy = self.copy_repository.get_by_id(transaction.get_copy_id()) @@ -92,11 +97,11 @@ def get_copy(self, transaction): return None - def check_version_copy_consistency(self, version, copy): + def check_version_copy_consistency(self, version: Version, copy: Copy | None) -> None: if (copy is not None and version.get_id() != copy.get_version_id()): raise InconsistentVersionAndCopyIdException(version.get_id(), copy.get_version_id()) - def update_copy_status(self, transaction, copy): + def update_copy_status(self, transaction: Transaction, copy: Copy | None) -> None: if copy is not None: # You cannot create an outbound transaction if you already don't have the copy anymore if transaction.get_type() in transaction.transaction_out and copy.get_status() == 'Out': @@ -107,7 +112,7 @@ def update_copy_status(self, transaction, copy): else: copy.set_status('Out') - def update_copy_in_db(self, transaction, copy): + def update_copy_in_db(self, transaction: Transaction, copy: Copy | None) -> None: if copy is not None: if self.is_sold_transaction(transaction): self.repository.reset_copy_id_for_transactions(copy.get_id(), False) @@ -115,17 +120,17 @@ def update_copy_in_db(self, transaction, copy): else: self.copy_repository.update(copy) - def is_sold_transaction(self, transaction): + def is_sold_transaction(self, transaction: Transaction) -> bool: if transaction.get_type() == 'Sold': return True return False - def update_copy_id(self, transaction): + def update_copy_id(self, transaction: Transaction) -> None: if self.is_sold_transaction(transaction): transaction.set_copy_id(None) - def check_no_duplicate_type_consecutive(self, copy, transaction): + def check_no_duplicate_type_consecutive(self, copy: Copy | None, transaction: Transaction) -> None: if copy is not None: # You cannot create the same transaction type (Inbound or Outbound type) twice last_transaction_for_copy = self.repository.get_last_transaction_for_copy(copy.get_id()) diff --git a/src/service/user_service.py b/src/service/user_service.py index 038e686..363cdfe 100644 --- a/src/service/user_service.py +++ b/src/service/user_service.py @@ -3,7 +3,10 @@ import random import string import base64 +from typing import Any + from flask import request + from src.exception.inactive_user_exception import InactiveUserException from src.exception.missing_field_exception import MissingFieldException from src.exception.missing_header_exception import MissingHeaderException @@ -16,19 +19,20 @@ from src.helpers.json_helper import JsonHelper from src.entity.user import User + class UserService: """Useless comment""" - def __init__(self, mysql): + def __init__(self, mysql: Any) -> None: self.user_repository = UserRepository(mysql) - def authenticate(self): + def authenticate(self) -> User: if 'Authorization' in request.headers: header_value = request.headers['Authorization'] if header_value.find(' ') != -1: array = header_value.split(' ') if array[0] == 'Basic': try: - decoded_value = base64.b64decode(array[1]) + decoded_value = base64.b64decode(array[1]) decoded_value = decoded_value.decode('utf-8') except UnicodeDecodeError: raise InvalidInput( @@ -42,7 +46,7 @@ def authenticate(self): raise MissingHeaderException('Authorization') - def get_by_filter(self, filter, filter_value): + def get_by_filter(self, filter: str, filter_value: int | str) -> User: if filter == 'id': user = self.user_repository.get_by_id(filter_value) elif filter == 'email': @@ -57,8 +61,8 @@ def get_by_filter(self, filter, filter_value): return user - def validate_payload_for_creation_and_hydrate(self): - values = [] + def validate_payload_for_creation_and_hydrate(self) -> User: + values: list[Any] = [] values.append(None) for api_field, data in User.expected_fields.items(): @@ -74,7 +78,7 @@ def validate_payload_for_creation_and_hydrate(self): values.append(None) values.append(None) - user = User(*values) # pylint: disable=E1120 + user = User(*values) # pylint: disable=E1120 check_user = self.user_repository.get_by_email(user.get_email()) if check_user is not None: @@ -86,7 +90,7 @@ def validate_payload_for_creation_and_hydrate(self): return user - def create(self, user): + def create(self, user: User) -> User | None: salt = self.get_new_salt() user.set_salt(salt) user.set_password(self.get_hashed_password(user.get_password(), salt)) @@ -94,7 +98,7 @@ def create(self, user): return self.user_repository.insert(user) - def update(self, user_id): + def update(self, user_id: int) -> User: # Verification user = self.user_repository.get_by_id(user_id) @@ -123,29 +127,29 @@ def update(self, user_id): return user - def get_new_salt(self): + def get_new_salt(self) -> str: return self.get_random_salt(8) - def get_new_token(self): + def get_new_token(self) -> str: return self.get_random_salt(32) - def renew_token(self, current_user): + def renew_token(self, current_user: User) -> None: current_user.set_token(self.get_new_token()) self.user_repository.update(current_user) @classmethod - def get_hashed_password(cls, password, salt): + def get_hashed_password(cls, password: str, salt: str) -> str: """Generates a hash from a given password and salt""" - password = hashlib.pbkdf2_hmac('sha256', bytes(password, 'utf-8'), bytes(salt, 'utf-8'), 4) - return password.hex() + hashed = hashlib.pbkdf2_hmac('sha256', bytes(password, 'utf-8'), bytes(salt, 'utf-8'), 4) + return hashed.hex() @classmethod - def get_random_salt(cls, string_length): + def get_random_salt(cls, string_length: int) -> str: """Genereates a salt""" letters = string.ascii_letters + string.digits return ''.join(random.choice(letters) for i in range(string_length)) - def get_authenticated_user(self, username, raw_password): + def get_authenticated_user(self, username: str, raw_password: str) -> User: """Load a user and check if the credentials are correct""" user = self.user_repository.get_by_user_name(username) if user is None: diff --git a/src/service/version_service.py b/src/service/version_service.py index 2f7c59f..e60fc83 100644 --- a/src/service/version_service.py +++ b/src/service/version_service.py @@ -1,3 +1,5 @@ +from typing import Any + from src.service.abstract_service import AbstractService from src.repository.version_repository import VersionRepository from src.repository.platform_repository import PlatformRepository @@ -9,16 +11,17 @@ from src.exception.resource_has_children_exception import RessourceHasChildrenException from src.helpers.json_helper import JsonHelper + class VersionService(AbstractService): resource_type = 'version' - def __init__(self, mysql): + def __init__(self, mysql: Any) -> None: self.repository = VersionRepository(mysql) self.game_repository = GameRepository(mysql) self.platform_repository = PlatformRepository(mysql) self.transaction_repository = TransactionRepository(mysql) - def get_for_create(self): + def get_for_create(self) -> Version: version = super().validate_payload_for_creation_and_hydrate(Version) platform = self.platform_repository.get_by_id(version.get_platform_id()) @@ -43,7 +46,7 @@ def get_for_create(self): return version - def get_for_update(self, version_id): + def get_for_update(self, version_id: int) -> Version: # Verification version = self.repository.get_by_id(version_id) @@ -72,7 +75,7 @@ def get_for_update(self, version_id): return version - def delete(self, version_id): + def delete(self, version_id: int) -> bool: """Delete a version""" version = self.repository.get_by_id(version_id) From 297b1033f9b51eed4d562cb5f3fa43b6829ece4c Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Mon, 13 Apr 2026 11:25:39 +0200 Subject: [PATCH 05/18] Improve MysQL factory and usage --- src/connection/mysql_factory.py | 38 +++++++------ src/repository/abstract_core_repository.py | 63 +++++++++++----------- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/src/connection/mysql_factory.py b/src/connection/mysql_factory.py index 1c0e714..10f8e19 100644 --- a/src/connection/mysql_factory.py +++ b/src/connection/mysql_factory.py @@ -1,33 +1,39 @@ +import threading from typing import Any from mysql import connector class MySQLFactory: + _local: threading.local = threading.local() @classmethod def init(cls, host: str, user: str, password: str, database: str) -> None: - cls.host = host - cls.user = user - cls.password = password - cls.database = database - cls.connection = None + cls._host = host + cls._user = user + cls._password = password + cls._database = database @classmethod def get(cls) -> Any: - """Get a connection""" - if cls.connection is None: - cls.connection = connector.connect( - host=cls.host, - user=cls.user, - passwd=cls.password, - database=cls.database + """Get a thread-local connection, creating or reconnecting if needed.""" + conn = getattr(cls._local, 'connection', None) + if conn is None: + conn = connector.connect( + host=cls._host, + user=cls._user, + passwd=cls._password, + database=cls._database ) + cls._local.connection = conn + else: + conn.ping(reconnect=True) - return cls.connection + return conn @classmethod def close(cls) -> None: - if cls.connection is not None: - cls.connection.close() - cls.connection = None + conn = getattr(cls._local, 'connection', None) + if conn is not None: + conn.close() + cls._local.connection = None diff --git a/src/repository/abstract_core_repository.py b/src/repository/abstract_core_repository.py index 4408305..993a94c 100644 --- a/src/repository/abstract_core_repository.py +++ b/src/repository/abstract_core_repository.py @@ -9,30 +9,28 @@ def __init__(self, mysql: Any) -> None: def fetch_one(self, request: str, data_tuple: tuple[Any, ...]) -> Any: """Fetch one result from a given request.""" cursor = self.mysql.cursor(dictionary=True) - cursor.execute(request, data_tuple) - row = cursor.fetchone() - - if row is None: - return None - - hydrated = self.hydrate(row) - cursor.close() - - return hydrated + try: + cursor.execute(request, data_tuple) + row = cursor.fetchone() + if row is None: + return None + return self.hydrate(row) + finally: + cursor.close() def fetch_multiple(self, request: str, data_tuple: tuple[Any, ...]) -> list[Any]: """Fetch mutliple items and return a list.""" items_list = [] cursor = self.mysql.cursor(dictionary=True, buffered=True) - cursor.execute(request, data_tuple) - - while True: - row = cursor.fetchone() - if row is None: - break - items_list.append(self.hydrate(row)) - - cursor.close() + try: + cursor.execute(request, data_tuple) + while True: + row = cursor.fetchone() + if row is None: + break + items_list.append(self.hydrate(row)) + finally: + cursor.close() return items_list def hydrate(self, row: dict[str, Any]) -> Any: @@ -50,14 +48,15 @@ def hydrate(self, row: dict[str, Any]) -> Any: def write(self, request: str, data: list[Any] | tuple[Any, ...], commit: bool = True) -> int | None: """Performs an UPDATE or WRITE statement""" cursor = self.mysql.cursor() - cursor.execute(request, data) - - if commit: - self.mysql.commit() - else: - self.mysql.autocommit = False - - return cursor.lastrowid + try: + cursor.execute(request, data) + if commit: + self.mysql.commit() + else: + self.mysql.autocommit = False + return cursor.lastrowid + finally: + cursor.close() def fetch_cursor(self, request: str, data_tuple: list[Any] | dict[str, Any] | None = None) -> dict[str, Any] | None: """Fetch one result""" @@ -65,8 +64,8 @@ def fetch_cursor(self, request: str, data_tuple: list[Any] | dict[str, Any] | No data_tuple = {} cursor = self.mysql.cursor(dictionary=True) - cursor.execute(request, data_tuple) - row = cursor.fetchone() - cursor.close() - - return row + try: + cursor.execute(request, data_tuple) + return cursor.fetchone() + finally: + cursor.close() From 11407b7dae811bdc518d6e46728ac47dcbbbc86f Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Mon, 13 Apr 2026 11:29:25 +0200 Subject: [PATCH 06/18] Improve fetch_multiple() method in the repository --- src/repository/abstract_core_repository.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/repository/abstract_core_repository.py b/src/repository/abstract_core_repository.py index 993a94c..88ba99b 100644 --- a/src/repository/abstract_core_repository.py +++ b/src/repository/abstract_core_repository.py @@ -20,18 +20,12 @@ def fetch_one(self, request: str, data_tuple: tuple[Any, ...]) -> Any: def fetch_multiple(self, request: str, data_tuple: tuple[Any, ...]) -> list[Any]: """Fetch mutliple items and return a list.""" - items_list = [] - cursor = self.mysql.cursor(dictionary=True, buffered=True) + cursor = self.mysql.cursor(dictionary=True) try: cursor.execute(request, data_tuple) - while True: - row = cursor.fetchone() - if row is None: - break - items_list.append(self.hydrate(row)) + return [self.hydrate(row) for row in cursor.fetchall()] finally: cursor.close() - return items_list def hydrate(self, row: dict[str, Any]) -> Any: """Hydrate an object from a row.""" From 007ce6a6c604b366196db8454c4d23b01b2700a9 Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Sat, 2 May 2026 16:53:28 +0200 Subject: [PATCH 07/18] Switch from CircleCI to GithubActions and add Magazine feature --- .circleci/config.yml | 72 -- .github/workflows/ci.yml | 87 ++ .gitignore | 2 + CLAUDE.md | 77 ++ README.md | 10 +- app.py | 144 +++ docker-compose.yml | 19 +- docker/data/nginx/nginx.conf | 4 +- migrations/5.0.0.sql | 95 ++ ...ame_version_magazine_mention_controller.py | 7 + src/controller/magazine_controller.py | 7 + src/controller/magazine_issue_controller.py | 7 + .../magazine_issue_copy_controller.py | 7 + src/entity/game_version_magazine_mention.py | 90 ++ src/entity/magazine.py | 58 + src/entity/magazine_issue.py | 94 ++ src/entity/magazine_issue_copy.py | 76 ++ ...ame_version_magazine_mention_repository.py | 7 + .../magazine_issue_copy_repository.py | 7 + src/repository/magazine_issue_repository.py | 40 + src/repository/magazine_repository.py | 35 + .../game_version_magazine_mention_service.py | 48 + src/service/magazine_issue_copy_service.py | 43 + src/service/magazine_issue_service.py | 63 + src/service/magazine_service.py | 55 + .../test_game_version_magazine_mentions.py | 97 ++ test/functional/test_magazine_issue_copies.py | 89 ++ test/functional/test_magazine_issues.py | 114 ++ test/functional/test_magazines.py | 104 ++ test/functional/test_notes.py | 2 +- test/games_test.sql | 1085 ++++++++++++++--- 31 files changed, 2379 insertions(+), 266 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/ci.yml create mode 100644 CLAUDE.md create mode 100644 migrations/5.0.0.sql create mode 100644 src/controller/game_version_magazine_mention_controller.py create mode 100644 src/controller/magazine_controller.py create mode 100644 src/controller/magazine_issue_controller.py create mode 100644 src/controller/magazine_issue_copy_controller.py create mode 100644 src/entity/game_version_magazine_mention.py create mode 100644 src/entity/magazine.py create mode 100644 src/entity/magazine_issue.py create mode 100644 src/entity/magazine_issue_copy.py create mode 100644 src/repository/game_version_magazine_mention_repository.py create mode 100644 src/repository/magazine_issue_copy_repository.py create mode 100644 src/repository/magazine_issue_repository.py create mode 100644 src/repository/magazine_repository.py create mode 100644 src/service/game_version_magazine_mention_service.py create mode 100644 src/service/magazine_issue_copy_service.py create mode 100644 src/service/magazine_issue_service.py create mode 100644 src/service/magazine_service.py create mode 100644 test/functional/test_game_version_magazine_mentions.py create mode 100644 test/functional/test_magazine_issue_copies.py create mode 100644 test/functional/test_magazine_issues.py create mode 100644 test/functional/test_magazines.py diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 8cafe1c..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: '2.1' -executors: - python: - docker: - - image: python:3.13 - name: python - - image: mysql:8.0 - name: mysql - environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: games - MYSQL_USER: game - MYSQL_PASSWORD: azerty - working_directory: ~/repo - resource_class: medium - -commands: - extra_checkout: - description: Add extra packages and apps. - steps: - - checkout - - run: - name: "Install system dependencies" - command: | - apt update && apt install -y netcat-traditional default-mysql-client git openssh-client curl make nano libzip-dev - - run: - name: "Install required Python packages" - command: | - pip install poetry && poetry config virtualenvs.create false && poetry install --no-root - - run: - name: "Create application configuration" - command: | - cp configuration.json.dist configuration.json - -jobs: - unittest: - executor: python - steps: - - extra_checkout - - run: - # Our primary container isn't MYSQL so run a sleep command until it's ready. - name: Waiting for MySQL to be ready - command: | - for i in `seq 1 90`; - do - nc -z mysql 3306 && echo Success && exit 0 - echo -n . - sleep 1 - done - echo Failed waiting for MySQL && exit 1 - - run: - name: Unit tests - command: | - mysql --skip-ssl -h mysql -u game -pazerty games < test/games_test.sql - nohup gunicorn --workers=1 --bind=0.0.0.0:9000 app:app & - sleep 5 - make test_command_python - - linter: - executor: python - steps: - - extra_checkout - - run: - name: Run pylint - command: pylint src/ ./app.py - -workflows: - version: '2.1' - Code quality: - jobs: - - linter - - unittest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..838c7fe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: Code quality + +on: + push: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install Poetry + run: pip install poetry + + - name: Install dependencies + run: poetry config virtualenvs.create false && poetry install --no-root + + - name: Run Ruff + run: ruff check src + + - name: Run Pylint + run: pylint src/ ./app.py + + test: + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} + MYSQL_DATABASE: games + MYSQL_USER: game + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=9 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install Poetry + run: pip install poetry + + - name: Install dependencies + run: poetry config virtualenvs.create false && poetry install --no-root + + - name: Create application configuration + run: | + cp configuration.json.dist configuration.json + sed -i 's/"db_host": "mysql"/"db_host": "127.0.0.1"/' configuration.json + + - name: Configure MySQL authentication + env: + MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + run: | + mysql --ssl-mode=DISABLED --get-server-public-key -h 127.0.0.1 -u root -p"$MYSQL_ROOT_PASSWORD" \ + -e "ALTER USER 'game'@'%' IDENTIFIED WITH mysql_native_password BY '$MYSQL_PASSWORD';" + + - name: Import test database + env: + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + run: mysql --ssl-mode=DISABLED -h 127.0.0.1 -u game -p"$MYSQL_PASSWORD" games < test/games_test.sql + + - name: Start application + run: nohup gunicorn --workers=1 --bind=0.0.0.0:9000 app:app & + + - name: Wait for application to be ready + run: sleep 5 + + - name: Run tests + run: python -m unittest discover . diff --git a/.gitignore b/.gitignore index a58c8c0..54236b4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ configuration.json .history/ .vscode/ .venv/ +.DS_Store + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..975d780 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,77 @@ +# Project Context + +This project is an API only application to manage a video games collection. It handles many elements, such as games, version, attributes of the games, the copy, transaction (selling or buying games), notes. + +It provides a basic REST+JSON api. + +## Architecture + +The main controller of the project is the _app.py_ file at the root of the project. Is also where everything is loaded (configuration, create of the SQL connection...). + +Then everything else (of the code) is stored in the _src_ folder. + +* _connection_ contains the class to manage the connection to the database; +* _controller_ contains each of the controllers specific to a resource type. Note that they have a base class that contains common method, for instance for CRUD operations; +* _entity_ contrains classes that represent ressources; +* _exception_ contains all the various exception classes; +* _helper_ contains various helpers; +* _repository_ contains repositories classes that allow to load and persist ressources. Note that they have two levels of base classes that contains shared logic to interact with the DB for the first one, and performs common operations (like loading, filtering...) for the second one; +* _service_ contains services classes, where "business logic" might be stored to keep controllers lightweight. + +## Entities + +The entities represent the ressources managed by the API. One file = one class = one ressource. + +Inside each entity class, we will find a constructor, getters and setters, and a method to serialize the object before returning it to the client. + +But the most important part is probably all the metadata at the beginning of the class. Let's take this example: + +``` + expected_fields: dict[str, Any] = { + 'title': {'field': 'title', 'method': '_title', 'required': True, 'type': 'text'}, + 'notes': { + 'field': 'notes', + 'method': '_notes', + 'required': False, + 'type': 'text', + 'default': '' + }, + } + + authorized_extra_fields_for_filtering: dict[str, Any] = { + 'id': {'field': 'id', 'origin': 'native', 'type': 'int'}, + 'versionCount': {'field': 'versionCount', 'origin': 'computed', 'type': 'int'} + } + + table_name = 'games' + primary_key = 'id' +``` + +* _expected_fields_ is an array that lists all the fields in the MySQL table. The key of the array is the key in the payload. For each field, we have a _field_ value that contains the name of the MySQL field. _method_ is the suffix of the getters and setters. For each field, you also have a _type_ metadata, which represents the data type, and also an optionnal _default_ key for optional values. +* _authorized_extra_fields_for_filtering_ is an array of the key in the URL that represent fields we can filter on. We have the type, but also the 'origin', which is 'native' or 'computed', the first case being when it is a direct filter on the value in the database. +* _table_name_ is the name of the MySQL table. +* _primary_key_ is the name of the primary key. + +Sometimes, things are more rigorous. +We have this case with the _copy_ entity. For some fields we can find things like this: + +``` +'type': 'strict-text', + 'allowed_values': { +``` +We notice that the type is "strict-text", hence we have a sub-array that contains all the allowed values for this field. We cannot list them all here, because they are specific to a field inside an entity. + +The allowd types are: +* _strict-text_: is a list of supported choices; +* _text_: a text of undefined sized; +* _int_: well, it is an integer; +* _string_: short text (varchar 255). + +The logical relation between the entities can be found in the _RESOURCES.md_ file in the _docs_ folder at the root of the project. + +## Running or testing the app locally + +* Running the app is not required for the AI agent. +* But testing it is usefull to detect added defects. Running the _make test_ command run the API test suite. +* Tests are located in the _tests_ folder. +* The stack uses _unittest_. diff --git a/README.md b/README.md index f1e3a5b..957aeed 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # GMG (Give Me a Game) -[![CircleCI](https://circleci.com/gh/ecourtial/gmg/tree/master.svg?style=svg)](https://circleci.com/gh/ecourtial/gmg/tree/master) [![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=gmg&metric=alert_status)](https://sonarcloud.io/dashboard?id=gmg) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/ecourtial/gmg/graphs/commit-activity) [![Ask Me Anything !](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg)](https://GitHub.com/ecourtial/gmg) [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/ecourtial/gmg/blob/master/LICENSE) +[![CI](https://github.com/ecourtial/gmg/actions/workflows/ci.yaml/badge.svg)](https://github.com/ecourtial/gmg/actions/workflows/ci.yaml) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/ecourtial/gmg/graphs/commit-activity) [![Ask Me Anything !](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg)](https://GitHub.com/ecourtial/gmg) [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/ecourtial/gmg/blob/master/LICENSE) ## Description :notebook: ### A back-end application for your video games inventory -GMG is an educational test project. Being a PHP programmer, I developed this project using Python 3.x and Flask 2. +GMG is an educational test project. Being a PHP programmer, I developed this project using Python 3.x and Flask 3.x. The goal of this application is to expose API endpoints to manage you video games collection, with various features. There is no graphical interfaces, only API endpoints. Data is stored in MySQL. @@ -34,12 +34,14 @@ A basic documentation is available: * Docker * Nginx * Gunicorn -* Python 3.10 -* Flask 2 +* Python 3 +* Flask 3 * Circle CI * unittest * pylint * MySQL 8 +* Ruff +* Poetry ## Changelog diff --git a/app.py b/app.py index 7f06353..b3b8899 100644 --- a/app.py +++ b/app.py @@ -11,6 +11,10 @@ from src.controller.story_controller import StoryController from src.controller.transaction_controller import TransactionController from src.controller.note_controller import NoteController +from src.controller.magazine_controller import MagazineController +from src.controller.magazine_issue_controller import MagazineIssueController +from src.controller.magazine_issue_copy_controller import MagazineIssueCopyController +from src.controller.game_version_magazine_mention_controller import GameVersionMagazineMentionController from src.repository.user_repository import UserRepository from src.connection.mysql_factory import MySQLFactory @@ -377,3 +381,143 @@ def get_notes() -> Response: """Get the notes""" controller = NoteController return controller.get_list(MySQLFactory.get()) + +# Magazines + +@app.route('/api/v1/magazine/', methods=['GET']) +def get_magazine_by_id(entity_id: int) -> tuple[Response, int]: + """Returns the magazine according to its id""" + controller = MagazineController + return controller.get_by_id(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/magazine', methods=['POST']) +@token_required +def create_magazine() -> tuple[Response, int]: + """Create a magazine""" + controller = MagazineController + return controller.create(MySQLFactory.get()) + +@app.route('/api/v1/magazine/', methods=['PATCH']) +@token_required +def update_magazine(entity_id: int) -> tuple[Response, int]: + """Update the magazine according to its id""" + controller = MagazineController + return controller.update(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/magazine/', methods=['DELETE']) +@token_required +def delete_magazine(entity_id: int) -> tuple[Response, int]: + """Delete the magazine according to its id""" + controller = MagazineController + return controller.delete(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/magazines', methods=['GET']) +def get_magazines() -> Response: + """Get the magazines""" + controller = MagazineController + return controller.get_list(MySQLFactory.get()) + +# Magazine Issues + +@app.route('/api/v1/magazine-issue/', methods=['GET']) +def get_magazine_issue_by_id(entity_id: int) -> tuple[Response, int]: + """Returns the magazine issue according to its id""" + controller = MagazineIssueController + return controller.get_by_id(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/magazine-issue', methods=['POST']) +@token_required +def create_magazine_issue() -> tuple[Response, int]: + """Create a magazine issue""" + controller = MagazineIssueController + return controller.create(MySQLFactory.get()) + +@app.route('/api/v1/magazine-issue/', methods=['PATCH']) +@token_required +def update_magazine_issue(entity_id: int) -> tuple[Response, int]: + """Update the magazine issue according to its id""" + controller = MagazineIssueController + return controller.update(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/magazine-issue/', methods=['DELETE']) +@token_required +def delete_magazine_issue(entity_id: int) -> tuple[Response, int]: + """Delete the magazine issue according to its id""" + controller = MagazineIssueController + return controller.delete(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/magazine-issues', methods=['GET']) +def get_magazine_issues() -> Response: + """Get the magazine issues""" + controller = MagazineIssueController + return controller.get_list(MySQLFactory.get()) + +# Magazine Issue Copies + +@app.route('/api/v1/magazine-issue-copy/', methods=['GET']) +def get_magazine_issue_copy_by_id(entity_id: int) -> tuple[Response, int]: + """Returns the magazine issue copy according to its id""" + controller = MagazineIssueCopyController + return controller.get_by_id(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/magazine-issue-copy', methods=['POST']) +@token_required +def create_magazine_issue_copy() -> tuple[Response, int]: + """Create a magazine issue copy""" + controller = MagazineIssueCopyController + return controller.create(MySQLFactory.get()) + +@app.route('/api/v1/magazine-issue-copy/', methods=['PATCH']) +@token_required +def update_magazine_issue_copy(entity_id: int) -> tuple[Response, int]: + """Update the magazine issue copy according to its id""" + controller = MagazineIssueCopyController + return controller.update(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/magazine-issue-copy/', methods=['DELETE']) +@token_required +def delete_magazine_issue_copy(entity_id: int) -> tuple[Response, int]: + """Delete the magazine issue copy according to its id""" + controller = MagazineIssueCopyController + return controller.delete(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/magazine-issue-copies', methods=['GET']) +def get_magazine_issue_copies() -> Response: + """Get the magazine issue copies""" + controller = MagazineIssueCopyController + return controller.get_list(MySQLFactory.get()) + +# Game Version Magazine Mentions + +@app.route('/api/v1/game-version-magazine-mention/', methods=['GET']) +def get_game_version_magazine_mention_by_id(entity_id: int) -> tuple[Response, int]: + """Returns the game version magazine mention according to its id""" + controller = GameVersionMagazineMentionController + return controller.get_by_id(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/game-version-magazine-mention', methods=['POST']) +@token_required +def create_game_version_magazine_mention() -> tuple[Response, int]: + """Create a game version magazine mention""" + controller = GameVersionMagazineMentionController + return controller.create(MySQLFactory.get()) + +@app.route('/api/v1/game-version-magazine-mention/', methods=['PATCH']) +@token_required +def update_game_version_magazine_mention(entity_id: int) -> tuple[Response, int]: + """Update the game version magazine mention according to its id""" + controller = GameVersionMagazineMentionController + return controller.update(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/game-version-magazine-mention/', methods=['DELETE']) +@token_required +def delete_game_version_magazine_mention(entity_id: int) -> tuple[Response, int]: + """Delete the game version magazine mention according to its id""" + controller = GameVersionMagazineMentionController + return controller.delete(MySQLFactory.get(), entity_id) + +@app.route('/api/v1/game-version-magazine-mentions', methods=['GET']) +def get_game_version_magazine_mentions() -> Response: + """Get the game version magazine mentions""" + controller = GameVersionMagazineMentionController + return controller.get_list(MySQLFactory.get()) diff --git a/docker-compose.yml b/docker-compose.yml index b5414dd..bada3b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: build: docker/nginx container_name: nginx_gmg ports: - - "80:80" + - "8000:80" volumes: - ./docker/data/nginx/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: @@ -35,3 +35,20 @@ services: - MYSQL_DATABASE=games - MYSQL_USER=game - MYSQL_PASSWORD=azerty + + ############################ + ## Adminer + ############################ + adminer: + image: adminer + container_name: adminer_gmg + ports: + - ${ADMINER_HTTP_PORT:-8080}:8080 + +############################ +## Common settings +############################ +volumes: + mysql_data: + driver: local + \ No newline at end of file diff --git a/docker/data/nginx/nginx.conf b/docker/data/nginx/nginx.conf index 5410511..142cb9a 100644 --- a/docker/data/nginx/nginx.conf +++ b/docker/data/nginx/nginx.conf @@ -33,7 +33,7 @@ http { server { # if no Host match, close the connection to prevent host spoofing listen 80 default_server; - return 444; + return 418; } server { @@ -43,7 +43,7 @@ http { client_max_body_size 4G; # set the correct host(s) for your site - server_name localhost; + server_name localhost host.docker.internal; keepalive_timeout 5; diff --git a/migrations/5.0.0.sql b/migrations/5.0.0.sql new file mode 100644 index 0000000..bb75826 --- /dev/null +++ b/migrations/5.0.0.sql @@ -0,0 +1,95 @@ +-- DROP the Foreign keys + +-- version_id versions(version_id) RESTRICT RESTRICT +ALTER TABLE `copies` DROP FOREIGN KEY `copies_ibfk_1`; + +-- version_id versions(version_id) RESTRICT RESTRICT +ALTER TABLE `stories` DROP FOREIGN KEY `stories_ibfk_1`; + +-- copy_id copies(copy_id) RESTRICT RESTRICT +ALTER TABLE `trades` DROP FOREIGN KEY `trades_ibfk_1`; + +-- copy_id copies(copy_id) RESTRICT RESTRICT +ALTER TABLE `transactions` DROP FOREIGN KEY `transactions_ibfk_1`; + +-- version_id versions(version_id) RESTRICT RESTRICT +ALTER TABLE `transactions` DROP FOREIGN KEY `transactions_ibfk_2`; + +-- game_id games(id) RESTRICT RESTRICT +ALTER TABLE `versions` DROP FOREIGN KEY `versions_ibfk_2`; + +-- UPDATE fields type + +ALTER TABLE `games` +CHANGE `id` `id` int unsigned NOT NULL AUTO_INCREMENT FIRST; + +ALTER TABLE `copies` +CHANGE `copy_id` `copy_id` int unsigned NOT NULL AUTO_INCREMENT FIRST, +CHANGE `version_id` `version_id` int unsigned NOT NULL AFTER `copy_id`; + +ALTER TABLE `trades` +CHANGE `copy_id` `copy_id` int unsigned NOT NULL AFTER `trade_id`; + +ALTER TABLE `transactions` +CHANGE `version_id` `version_id` int unsigned NOT NULL AFTER `transaction_id`, +CHANGE `copy_id` `copy_id` int unsigned NULL AFTER `version_id`; + +ALTER TABLE `versions` +CHANGE `version_id` `version_id` int unsigned NOT NULL AUTO_INCREMENT FIRST, +CHANGE `game_id` `game_id` int unsigned NOT NULL AFTER `platform_id`; + +ALTER TABLE `stories` +CHANGE `version_id` `version_id` int unsigned NULL AFTER `id`; + +-- RECREATE foreign keys + +ALTER TABLE `copies` ADD FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) ON DELETE RESTRICT ON UPDATE RESTRICT; + +ALTER TABLE `stories` ADD FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) ON DELETE RESTRICT ON UPDATE RESTRICT; + +ALTER TABLE `trades` ADD FOREIGN KEY (`copy_id`) REFERENCES `copies` (`copy_id`) ON DELETE RESTRICT ON UPDATE RESTRICT; + +ALTER TABLE `transactions` ADD FOREIGN KEY (`copy_id`) REFERENCES `copies` (`copy_id`) ON DELETE RESTRICT ON UPDATE RESTRICT; + +ALTER TABLE `transactions` ADD FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) ON DELETE RESTRICT ON UPDATE RESTRICT; + +ALTER TABLE `versions` ADD FOREIGN KEY (`game_id`) REFERENCES `games` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT; + +-- ADD new tables + +CREATE TABLE `magazines` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `title` TEXT NOT NULL, + `notes` TEXT NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `magazine_issues` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `magazine_id` INT UNSIGNED NOT NULL, + `issue_number` SMALLINT UNSIGNED NOT NULL, + `year` SMALLINT UNSIGNED NOT NULL, + `month` TINYINT UNSIGNED NOT NULL, + `notes` TEXT NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_magazine_issue_magazine` FOREIGN KEY (`magazine_id`) REFERENCES `magazines` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `magazine_issue_copies` ( + `copy_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `magazine_issue_id` INT UNSIGNED NOT NULL, + `type` VARCHAR(255) NOT NULL, + PRIMARY KEY (`copy_id`), + CONSTRAINT `fk_magazine_issue_copy_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE `game_version_magazine_mentions` ( + `mention_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `magazine_issue_id` INT UNSIGNED NOT NULL, + `game_version_id` INT UNSIGNED NOT NULL, + `type` VARCHAR(255) NOT NULL, + `notes` TEXT NOT NULL, + PRIMARY KEY (`mention_id`), + CONSTRAINT `fk_mention_magazine_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`), + CONSTRAINT `fk_mention_game_version` FOREIGN KEY (`game_version_id`) REFERENCES `versions` (`version_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/src/controller/game_version_magazine_mention_controller.py b/src/controller/game_version_magazine_mention_controller.py new file mode 100644 index 0000000..9d28439 --- /dev/null +++ b/src/controller/game_version_magazine_mention_controller.py @@ -0,0 +1,7 @@ +from src.controller.abstract_controller import AbstractController +from src.repository.game_version_magazine_mention_repository import GameVersionMagazineMentionRepository +from src.service.game_version_magazine_mention_service import GameVersionMagazineMentionService + +class GameVersionMagazineMentionController(AbstractController): + repository = GameVersionMagazineMentionRepository + service = GameVersionMagazineMentionService diff --git a/src/controller/magazine_controller.py b/src/controller/magazine_controller.py new file mode 100644 index 0000000..53b5846 --- /dev/null +++ b/src/controller/magazine_controller.py @@ -0,0 +1,7 @@ +from src.controller.abstract_controller import AbstractController +from src.repository.magazine_repository import MagazineRepository +from src.service.magazine_service import MagazineService + +class MagazineController(AbstractController): + repository = MagazineRepository + service = MagazineService diff --git a/src/controller/magazine_issue_controller.py b/src/controller/magazine_issue_controller.py new file mode 100644 index 0000000..5f7cff7 --- /dev/null +++ b/src/controller/magazine_issue_controller.py @@ -0,0 +1,7 @@ +from src.controller.abstract_controller import AbstractController +from src.repository.magazine_issue_repository import MagazineIssueRepository +from src.service.magazine_issue_service import MagazineIssueService + +class MagazineIssueController(AbstractController): + repository = MagazineIssueRepository + service = MagazineIssueService diff --git a/src/controller/magazine_issue_copy_controller.py b/src/controller/magazine_issue_copy_controller.py new file mode 100644 index 0000000..0016ca3 --- /dev/null +++ b/src/controller/magazine_issue_copy_controller.py @@ -0,0 +1,7 @@ +from src.controller.abstract_controller import AbstractController +from src.repository.magazine_issue_copy_repository import MagazineIssueCopyRepository +from src.service.magazine_issue_copy_service import MagazineIssueCopyService + +class MagazineIssueCopyController(AbstractController): + repository = MagazineIssueCopyRepository + service = MagazineIssueCopyService diff --git a/src/entity/game_version_magazine_mention.py b/src/entity/game_version_magazine_mention.py new file mode 100644 index 0000000..d54b4f5 --- /dev/null +++ b/src/entity/game_version_magazine_mention.py @@ -0,0 +1,90 @@ +from typing import Any + +from src.entity.abstract_entity import AbstractEntity + +class GameVersionMagazineMention(AbstractEntity): + """ This class represent when a versioon of a game is mentioned the issue of a magazine. """ + # If you change the order here, you need to also change it in the constructor! + expected_fields: dict[str, Any] = { + 'magazineIssueId': { + 'field': 'magazine_issue_id', + 'method': '_magazine_issue_id', + 'required': True, + 'type': 'int' + }, + 'gameVersionId': { + 'field': 'game_version_id', + 'method': '_game_version_id', + 'required': True, + 'type': 'int' + }, + 'type': { + 'field': 'type', + 'method': '_type', + 'required': True, + 'type': 'strict-text', + 'allowed_values': {'Preview', 'Test', 'Guide', 'Other', 'Playable-demo', 'Watchable-demo'} + }, + 'notes': { + 'field': 'notes', + 'method': '_notes', + 'required': False, + 'type': 'text', + }, + } + + authorized_extra_fields_for_filtering: dict[str, Any] = { + 'id': {'field': 'mention_id', 'origin': 'native', 'type': 'int'}, + 'magazineIssueId': {'field': 'magazine_issue_id', 'origin': 'native', 'type': 'int'}, + 'gameVersionId': {'field': 'game_version_id', 'origin': 'native', 'type': 'int'}, + 'type': {'field': 'type', 'origin': 'native', 'type': 'string'}, + } + + table_name = 'game_version_magazine_mentions' + primary_key = 'mention_id' + + # If you change the order here, you need to also change it in the array above! + def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments + self, + entity_id: int | None, + magazine_issue_id: int, + game_version_id: int, + type: str, + notes: str + ) -> None: + self.entity_id = entity_id + self.magazine_issue_id = int(magazine_issue_id) + self.game_version_id = int(game_version_id) + self.type = type + self.notes = notes + + def get_id(self) -> int | None: + return self.entity_id + + def get_magazine_issue_id(self) -> int: + return self.magazine_issue_id + + def set_magazine_issue_id(self, magazine_issue_id: int) -> None: + self.magazine_issue_id = int(magazine_issue_id) + + def get_game_version_id(self) -> int: + return self.game_version_id + + def set_game_version_id(self, game_version_id: int) -> None: + self.game_version_id = int(game_version_id) + + def get_type(self) -> str: + return self.type + + def set_type(self, type: str) -> None: + self.type = type + + def get_notes(self) -> str: + return self.notes + + def set_notes(self, notes: str) -> None: + self.notes = notes + + def serialize(self) -> dict[str, Any]: + values = super().serialize() + return values diff --git a/src/entity/magazine.py b/src/entity/magazine.py new file mode 100644 index 0000000..c1cbdbf --- /dev/null +++ b/src/entity/magazine.py @@ -0,0 +1,58 @@ +from typing import Any + +from src.entity.abstract_entity import AbstractEntity + +class Magazine(AbstractEntity): + """ This class represent a magazine, for instance "PC Gamer" """ + + expected_fields: dict[str, Any] = { + 'title': {'field': 'title', 'method': '_title', 'required': True, 'type': 'text'}, + 'notes': { + 'field': 'notes', + 'method': '_notes', + 'required': False, + 'type': 'text', + }, + } + + authorized_extra_fields_for_filtering: dict[str, Any] = { + 'id': {'field': 'id', 'origin': 'native', 'type': 'int'}, + 'issueCount': {'field': 'issueCount', 'origin': 'computed', 'type': 'int'} + } + + table_name = 'magazines' + primary_key = 'id' + + def __init__(self, entity_id: int | None, title: str, notes: str, issue_count: int | None = None) -> None: + self.entity_id = entity_id + self.title = title + self.notes = notes + self.issue_count = int(issue_count or 0) + + def get_id(self) -> int | None: + """Return the id of the magazine, for instance "125".""" + return self.entity_id + + def get_title(self) -> str: + """Return the title of the magazine, for instance "PC Gamer".""" + return self.title + + def set_title(self, title: str) -> None: + self.title = title + + def get_notes(self) -> str: + return self.notes + + def set_notes(self, notes: str) -> None: + self.notes = notes + + def get_issue_count(self) -> int: + return self.issue_count + + def set_issue_count(self, issue_count: int | None) -> None: + self.issue_count = int(issue_count or 0) + + def serialize(self) -> dict[str, Any]: + values = super().serialize() + values['issueCount'] = self.get_issue_count() + return values diff --git a/src/entity/magazine_issue.py b/src/entity/magazine_issue.py new file mode 100644 index 0000000..749f146 --- /dev/null +++ b/src/entity/magazine_issue.py @@ -0,0 +1,94 @@ +from typing import Any + +from src.entity.abstract_entity import AbstractEntity + +class MagazineIssue(AbstractEntity): + """ This class represent a magazine issue, for instance "Issue #39" """ + + expected_fields: dict[str, Any] = { + 'magazineId': { + 'field': 'magazine_id', + 'method': '_magazine_id', + 'required': True, + 'type': 'int' + }, + 'issueNumber': {'field': 'issue_number', 'method': '_issue_number', 'required': True, 'type': 'int'}, + 'year': {'field': 'year', 'method': '_year', 'required': True, 'type': 'int'}, + 'month': {'field': 'month', 'method': '_month', 'required': True, 'type': 'int'}, + 'notes': { + 'field': 'notes', + 'method': '_notes', + 'required': False, + 'type': 'text', + }, + } + + authorized_extra_fields_for_filtering: dict[str, Any] = { + 'id': {'field': 'id', 'origin': 'native', 'type': 'int'}, + 'magazineId': {'field': 'magazine_id', 'origin': 'native', 'type': 'int'}, + 'year': {'field': 'year', 'origin': 'native', 'type': 'int'}, + 'month': {'field': 'month', 'origin': 'native', 'type': 'int'}, + } + + table_name = 'magazine_issues' + primary_key = 'id' + + def __init__(self, entity_id: int | None, magazine_id: int, issue_number:int, year: int, month: int, notes: str, copy_count: int | None = None, mention_count: int | None = None) -> None: + self.entity_id = entity_id + self.issue_number = issue_number + self.notes = notes + self.magazine_id = magazine_id + self.year = year + self.month = month + self.copy_count = int(copy_count or 0) + self.mention_count = int(mention_count or 0) + + def get_id(self) -> int | None: + """Return the id of the issue, for instance "125".""" + return self.entity_id + + def get_notes(self) -> str: + return self.notes + + def set_notes(self, notes: str) -> None: + self.notes = notes + + def get_magazine_id(self)-> int: + return self.magazine_id + + def set_magazine_id(self, magazine_id: int)-> None: + self.magazine_id = magazine_id + + def get_issue_number(self)-> int: + return self.issue_number + + def set_issue_number(self, issue_number:int )-> None: + self.issue_number = issue_number + + def get_month(self)-> int: + return self.month + + def set_month(self, month: int)-> None: + self.month = month + + def get_year(self)-> int: + return self.year + + def set_year(self, year: int)-> None: + self.year = year + + def get_copy_count(self) -> int: + return self.copy_count + + def set_copy_count(self, copy_count: int | None) -> None: + self.copy_count = int(copy_count or 0) + + def get_mention_count(self) -> int: + return self.mention_count + + def set_mention_count(self, mention_count: int | None) -> None: + self.mention_count = int(mention_count or 0) + + def serialize(self) -> dict[str, Any]: + values = super().serialize() + return values diff --git a/src/entity/magazine_issue_copy.py b/src/entity/magazine_issue_copy.py new file mode 100644 index 0000000..ad1f9fc --- /dev/null +++ b/src/entity/magazine_issue_copy.py @@ -0,0 +1,76 @@ +from typing import Any + +from src.entity.abstract_entity import AbstractEntity + +class MagazineIssueCopy(AbstractEntity): + """ This class represent a copy of the issue of a magazine. """ + # If you change the order here, you need to also change it in the constructor! + expected_fields: dict[str, Any] = { + 'magazineIssueId': + { + 'field': 'magazine_issue_id', + 'method': '_magazine_issue_id', + 'required': True, + 'type': 'int' + }, + 'type': { + 'field': 'type', + 'method': '_type', + 'required': True, + 'type': 'strict-text', + 'allowed_values': {'Digital', 'Paper'} + }, + 'notes': { + 'field': 'notes', + 'method': '_notes', + 'required': False, + 'type': 'text', + }, + } + + authorized_extra_fields_for_filtering: dict[str, Any] = { + 'id': {'field': 'issue_copy_id', 'origin': 'native', 'type': 'int'}, + 'magazineIssueId': {'field': 'magazine_issue_id', 'origin': 'native', 'type': 'int'}, + 'type': {'field': 'type', 'origin': 'native', 'type': 'string'}, + } + + table_name = 'magazine_issue_copies' + primary_key = 'issue_copy_id' + + # If you change the order here, you need to also change it in the array above! + def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments + self, + entity_id: int | None, + magazine_issue_id: int, + type: str, + notes: str + ) -> None: + self.entity_id = entity_id + self.magazine_issue_id = int(magazine_issue_id) + self.type = type + self.notes = notes + + def get_id(self) -> int | None: + return self.entity_id + + def get_magazine_issue_id(self) -> int: + return self.magazine_issue_id + + def set_magazine_issue_id(self, magazine_issue_id: int) -> None: + self.magazine_issue_id = int(magazine_issue_id) + + def get_type(self) -> str: + return self.type + + def set_type(self, type: str) -> None: + self.type = type + + def get_notes(self) -> str: + return self.notes + + def set_notes(self, notes: str) -> None: + self.notes = notes + + def serialize(self) -> dict[str, Any]: + values = super().serialize() + return values diff --git a/src/repository/game_version_magazine_mention_repository.py b/src/repository/game_version_magazine_mention_repository.py new file mode 100644 index 0000000..8596afa --- /dev/null +++ b/src/repository/game_version_magazine_mention_repository.py @@ -0,0 +1,7 @@ +""" Repository to handle the game version magazine mentions """ +from src.repository.abstract_repository import AbstractRepository +from src.entity.game_version_magazine_mention import GameVersionMagazineMention + + +class GameVersionMagazineMentionRepository(AbstractRepository): + entity = GameVersionMagazineMention diff --git a/src/repository/magazine_issue_copy_repository.py b/src/repository/magazine_issue_copy_repository.py new file mode 100644 index 0000000..9a8bb6c --- /dev/null +++ b/src/repository/magazine_issue_copy_repository.py @@ -0,0 +1,7 @@ +""" Repository to handle the magazine issue copies """ +from src.repository.abstract_repository import AbstractRepository +from src.entity.magazine_issue_copy import MagazineIssueCopy + + +class MagazineIssueCopyRepository(AbstractRepository): + entity = MagazineIssueCopy diff --git a/src/repository/magazine_issue_repository.py b/src/repository/magazine_issue_repository.py new file mode 100644 index 0000000..1472bd6 --- /dev/null +++ b/src/repository/magazine_issue_repository.py @@ -0,0 +1,40 @@ +""" Repository to handle the magazine issues """ +from typing import Any + +from src.repository.abstract_repository import AbstractRepository +from src.entity.magazine_issue import MagazineIssue +from src.entity.magazine_issue_copy import MagazineIssueCopy +from src.entity.game_version_magazine_mention import GameVersionMagazineMention + + +class MagazineIssueRepository(AbstractRepository): + entity = MagazineIssue + + def get_select_request_start(self) -> str: + t = MagazineIssue.table_name + pk = MagazineIssue.primary_key + copies = MagazineIssueCopy.table_name + mentions = GameVersionMagazineMention.table_name + + request = f"SELECT {t}.*, c.copyCount AS copyCount, m.mentionCount AS mentionCount " + request += f"FROM {t} " + request += f"LEFT JOIN (SELECT magazine_issue_id, COUNT(*) AS copyCount FROM {copies} GROUP BY magazine_issue_id) AS c ON c.magazine_issue_id = {t}.{pk} " + request += f"LEFT JOIN (SELECT magazine_issue_id, COUNT(*) AS mentionCount FROM {mentions} GROUP BY magazine_issue_id) AS m ON m.magazine_issue_id = {t}.{pk} " + request += f"WHERE {t}.{pk} IS NOT NULL " + + return request + + def hydrate(self, row: dict[str, Any]) -> MagazineIssue: + issue = super().hydrate(row) + issue.set_copy_count(row['copyCount']) + issue.set_mention_count(row['mentionCount']) + + return issue + + def get_by_magazine_and_issue_number(self, magazine_id: int, issue_number: int) -> MagazineIssue | None: + """Get one issue by its magazine and issue number.""" + request = self.get_select_request_start() + request += f"AND {MagazineIssue.table_name}.magazine_id = %s " + request += f"AND {MagazineIssue.table_name}.issue_number = %s LIMIT 1;" + + return self.fetch_one(request, (magazine_id, issue_number)) diff --git a/src/repository/magazine_repository.py b/src/repository/magazine_repository.py new file mode 100644 index 0000000..87ed602 --- /dev/null +++ b/src/repository/magazine_repository.py @@ -0,0 +1,35 @@ +""" Repository to handle the magazines """ +from typing import Any + +from src.repository.abstract_repository import AbstractRepository +from src.entity.magazine import Magazine +from src.entity.magazine_issue import MagazineIssue + + +class MagazineRepository(AbstractRepository): + entity = Magazine + + def get_select_request_start(self) -> str: + request = f"SELECT {Magazine.table_name}.*, i.issueCount AS issueCount " + request += 'FROM ' + request += f" (SELECT COUNT(*) AS issueCount, {Magazine.table_name}.id AS magazine_id " + request += f" FROM {MagazineIssue.table_name}, {Magazine.table_name} " + request += f" WHERE {MagazineIssue.table_name}.magazine_id = {Magazine.table_name}.{Magazine.primary_key} " + request += f" GROUP BY {Magazine.table_name}.{Magazine.primary_key}) AS i " + request += f"RIGHT JOIN {Magazine.table_name} ON " + request += f"{Magazine.table_name}.{Magazine.primary_key} = i.magazine_id WHERE TRUE " + + return request + + def get_by_title(self, title: str) -> Magazine | None: + """Get one magazine by its title.""" + request = self.get_select_request_start() + f"AND {Magazine.table_name}.title = %s LIMIT 1;" + + return self.fetch_one(request, (title,)) + + def hydrate(self, row: dict[str, Any]) -> Magazine: + """Hydrate an object from a row.""" + magazine = super().hydrate(row) + magazine.set_issue_count(row['issueCount']) + + return magazine diff --git a/src/service/game_version_magazine_mention_service.py b/src/service/game_version_magazine_mention_service.py new file mode 100644 index 0000000..e0c4b61 --- /dev/null +++ b/src/service/game_version_magazine_mention_service.py @@ -0,0 +1,48 @@ +from typing import Any + +from src.service.abstract_service import AbstractService +from src.repository.game_version_magazine_mention_repository import GameVersionMagazineMentionRepository +from src.repository.magazine_issue_repository import MagazineIssueRepository +from src.repository.version_repository import VersionRepository +from src.entity.game_version_magazine_mention import GameVersionMagazineMention +from src.exception.unknown_resource_exception import ResourceNotFoundException + + +class GameVersionMagazineMentionService(AbstractService): + resource_type = 'game_version_magazine_mention' + + def __init__(self, mysql: Any) -> None: + self.repository = GameVersionMagazineMentionRepository(mysql) + self.magazine_issue_repository = MagazineIssueRepository(mysql) + self.version_repository = VersionRepository(mysql) + + def get_for_create(self) -> GameVersionMagazineMention: + mention = super().validate_payload_for_creation_and_hydrate(GameVersionMagazineMention) + + if self.magazine_issue_repository.get_by_id(mention.get_magazine_issue_id()) is None: + raise ResourceNotFoundException('magazine_issue', mention.get_magazine_issue_id()) + + if self.version_repository.get_by_id(mention.get_game_version_id()) is None: + raise ResourceNotFoundException('version', mention.get_game_version_id()) + + return mention + + def get_for_update(self, mention_id: int) -> GameVersionMagazineMention: + mention = self.repository.get_by_id(mention_id) + + if mention is None: + raise ResourceNotFoundException('game_version_magazine_mention', mention_id) + + super().hydrate_for_update(mention) + + return mention + + def delete(self, mention_id: int) -> bool: + mention = self.repository.get_by_id(mention_id) + + if mention is None: + raise ResourceNotFoundException('game_version_magazine_mention', mention_id) + + self.repository.delete(mention_id) + + return True diff --git a/src/service/magazine_issue_copy_service.py b/src/service/magazine_issue_copy_service.py new file mode 100644 index 0000000..a2304df --- /dev/null +++ b/src/service/magazine_issue_copy_service.py @@ -0,0 +1,43 @@ +from typing import Any + +from src.service.abstract_service import AbstractService +from src.repository.magazine_issue_copy_repository import MagazineIssueCopyRepository +from src.repository.magazine_issue_repository import MagazineIssueRepository +from src.entity.magazine_issue_copy import MagazineIssueCopy +from src.exception.unknown_resource_exception import ResourceNotFoundException + + +class MagazineIssueCopyService(AbstractService): + resource_type = 'magazine_issue_copy' + + def __init__(self, mysql: Any) -> None: + self.repository = MagazineIssueCopyRepository(mysql) + self.magazine_issue_repository = MagazineIssueRepository(mysql) + + def get_for_create(self) -> MagazineIssueCopy: + copy = super().validate_payload_for_creation_and_hydrate(MagazineIssueCopy) + + if self.magazine_issue_repository.get_by_id(copy.get_magazine_issue_id()) is None: + raise ResourceNotFoundException('magazine_issue', copy.get_magazine_issue_id()) + + return copy + + def get_for_update(self, copy_id: int) -> MagazineIssueCopy: + copy = self.repository.get_by_id(copy_id) + + if copy is None: + raise ResourceNotFoundException('magazine_issue_copy', copy_id) + + super().hydrate_for_update(copy) + + return copy + + def delete(self, copy_id: int) -> bool: + copy = self.repository.get_by_id(copy_id) + + if copy is None: + raise ResourceNotFoundException('magazine_issue_copy', copy_id) + + self.repository.delete(copy_id) + + return True diff --git a/src/service/magazine_issue_service.py b/src/service/magazine_issue_service.py new file mode 100644 index 0000000..dcbc59b --- /dev/null +++ b/src/service/magazine_issue_service.py @@ -0,0 +1,63 @@ +from typing import Any + +from src.service.abstract_service import AbstractService +from src.repository.magazine_issue_repository import MagazineIssueRepository +from src.repository.magazine_repository import MagazineRepository +from src.entity.magazine_issue import MagazineIssue +from src.exception.resource_already_exists_exception import ResourceAlreadyExistsException +from src.exception.unknown_resource_exception import ResourceNotFoundException +from src.exception.resource_has_children_exception import RessourceHasChildrenException +from src.helpers.json_helper import JsonHelper + + +class MagazineIssueService(AbstractService): + resource_type = 'magazine_issue' + + def __init__(self, mysql: Any) -> None: + self.repository = MagazineIssueRepository(mysql) + self.magazine_repository = MagazineRepository(mysql) + + def get_for_create(self) -> MagazineIssue: + issue = super().validate_payload_for_creation_and_hydrate(MagazineIssue) + + if self.magazine_repository.get_by_id(issue.get_magazine_id()) is None: + raise ResourceNotFoundException('magazine', issue.get_magazine_id()) + + existing = self.repository.get_by_magazine_and_issue_number(issue.get_magazine_id(), issue.get_issue_number()) + if existing is not None: + raise ResourceAlreadyExistsException('magazine_issue', str(issue.get_issue_number()), 'issue_number') + + return issue + + def get_for_update(self, issue_id: int) -> MagazineIssue: + issue = self.repository.get_by_id(issue_id) + + if issue is None: + raise ResourceNotFoundException('magazine_issue', issue_id) + + magazine_id = int(JsonHelper.get_value_from_request('magazineId', issue.get_magazine_id())) + issue_number = int(JsonHelper.get_value_from_request('issueNumber', issue.get_issue_number())) + + existing = self.repository.get_by_magazine_and_issue_number(magazine_id, issue_number) + if existing is not None and existing.get_id() != issue.get_id(): + raise ResourceAlreadyExistsException('magazine_issue', str(issue_number), 'issue_number') + + super().hydrate_for_update(issue) + + return issue + + def delete(self, issue_id: int) -> bool: + issue = self.repository.get_by_id(issue_id) + + if issue is None: + raise ResourceNotFoundException('magazine_issue', issue_id) + + if issue.get_copy_count() > 0: + raise RessourceHasChildrenException('magazine_issue', 'copy') + + if issue.get_mention_count() > 0: + raise RessourceHasChildrenException('magazine_issue', 'game_version_magazine_mention') + + self.repository.delete(issue_id) + + return True diff --git a/src/service/magazine_service.py b/src/service/magazine_service.py new file mode 100644 index 0000000..15604cc --- /dev/null +++ b/src/service/magazine_service.py @@ -0,0 +1,55 @@ +from typing import Any + +from src.service.abstract_service import AbstractService +from src.repository.magazine_repository import MagazineRepository +from src.entity.magazine import Magazine +from src.exception.resource_already_exists_exception import ResourceAlreadyExistsException +from src.exception.unknown_resource_exception import ResourceNotFoundException +from src.exception.resource_has_children_exception import RessourceHasChildrenException +from src.helpers.json_helper import JsonHelper + + +class MagazineService(AbstractService): + resource_type = 'magazine' + + def __init__(self, mysql: Any) -> None: + self.repository = MagazineRepository(mysql) + + def get_for_create(self) -> Magazine: + magazine = super().validate_payload_for_creation_and_hydrate(Magazine) + + existing = self.repository.get_by_title(magazine.get_title()) + + if existing is not None: + raise ResourceAlreadyExistsException('magazine', magazine.get_title(), 'title') + + return magazine + + def get_for_update(self, magazine_id: int) -> Magazine: + magazine = self.repository.get_by_id(magazine_id) + + if magazine is None: + raise ResourceNotFoundException('magazine', magazine_id) + + title = JsonHelper.get_value_from_request('title', magazine.get_title()) + existing = self.repository.get_by_title(title) + + if existing is not None and existing.get_id() != magazine.get_id(): + raise ResourceAlreadyExistsException('magazine', magazine.get_title(), 'title') + + super().hydrate_for_update(magazine) + + return magazine + + def delete(self, magazine_id: int) -> bool: + magazine = self.repository.get_by_id(magazine_id) + + if magazine is None: + raise ResourceNotFoundException('magazine', magazine_id) + + if magazine.get_issue_count() > 0: + raise RessourceHasChildrenException('magazine', 'issue') + + self.repository.delete(magazine_id) + + return True diff --git a/test/functional/test_game_version_magazine_mentions.py b/test/functional/test_game_version_magazine_mentions.py new file mode 100644 index 0000000..040dd28 --- /dev/null +++ b/test/functional/test_game_version_magazine_mentions.py @@ -0,0 +1,97 @@ +from test.abstract_tests import AbstractTests + +class TestGameVersionMagazineMentions(AbstractTests): + def test_commons(self): + super().check_all_routes_error_bad_user_token('game-version-magazine-mention') + super().check_all_routes_error_missing_user_token('game-version-magazine-mention') + + def test_get_mention(self): + # Does not exist + resp = self.api_call('get', 'game-version-magazine-mention/666', {}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'game_version_magazine_mention' with id #666 has not been found.", 'code': 1}, resp.json()) + + # Exists + resp = self.api_call('get', 'game-version-magazine-mention/1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual({'id': 1, 'magazineIssueId': 1, 'gameVersionId': 1, 'type': 'Test', 'notes': ''}, resp.json()) + + def test_create_incomplete_payload(self): + resp = self.api_call('post', 'game-version-magazine-mention', {}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': 'The following field is missing: magazineIssueId.', 'code': 6}, resp.json()) + + def test_create_unsupported_type(self): + resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 1, 'gameVersionId': 1, 'type': 'Review'}, True) + + self.assertEqual(400, resp.status_code) + + def test_create_magazine_issue_not_found(self): + resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 666, 'gameVersionId': 1, 'type': 'Test'}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) + + def test_create_game_version_not_found(self): + resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 1, 'gameVersionId': 666, 'type': 'Test'}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'version' with id #666 has not been found.", 'code': 1}, resp.json()) + + def test_create_update_delete_success(self): + # Create + payload = {'magazineIssueId': 1, 'gameVersionId': 1, 'type': 'Preview', 'notes': 'A preview mention.'} + resp = self.api_call('post', 'game-version-magazine-mention', payload, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual('Preview', resp.json()['type']) + mention_id = str(resp.json()['id']) + + resp = self.api_call('get', 'game-version-magazine-mention/' + mention_id, None, True) + payload['id'] = 3 + self.assertEqual(payload, resp.json()) + + # Patch + resp = self.api_call('patch', 'game-version-magazine-mention/' + mention_id, {'notes': 'Updated.'}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual('Updated.', resp.json()['notes']) + + # Delete + resp = self.api_call('delete', 'game-version-magazine-mention/' + mention_id, {}, True) + self.assertEqual(200, resp.status_code) + + resp = self.api_call('delete', 'game-version-magazine-mention/' + mention_id, {}, True) + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': f"The resource of type 'game_version_magazine_mention' with id #{mention_id} has not been found.", 'code': 1}, resp.json()) + + def test_update_not_found(self): + resp = self.api_call('patch', 'game-version-magazine-mention/666', {'notes': 'Whatever'}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'game_version_magazine_mention' with id #666 has not been found.", 'code': 1}, resp.json()) + + def test_get_list_default_filters(self): + resp = self.api_call('get', 'game-version-magazine-mentions', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(2, resp.json()['resultCount']) + self.assertEqual(2, resp.json()['totalResultCount']) + self.assertEqual(1, resp.json()['page']) + self.assertEqual(1, resp.json()['totalPageCount']) + + def test_get_list_filter_by_magazine_issue(self): + resp = self.api_call('get', 'game-version-magazine-mentions?magazineIssueId[]=1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(1, resp.json()['resultCount']) + self.assertEqual(1, resp.json()['result'][0]['id']) + + def test_get_list_filter_by_game_version(self): + resp = self.api_call('get', 'game-version-magazine-mentions?gameVersionId[]=1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(2, resp.json()['resultCount']) diff --git a/test/functional/test_magazine_issue_copies.py b/test/functional/test_magazine_issue_copies.py new file mode 100644 index 0000000..9cb2565 --- /dev/null +++ b/test/functional/test_magazine_issue_copies.py @@ -0,0 +1,89 @@ +from test.abstract_tests import AbstractTests + +class TestMagazineIssueCopies(AbstractTests): + def test_commons(self): + super().check_all_routes_error_bad_user_token('magazine-issue-copy') + super().check_all_routes_error_missing_user_token('magazine-issue-copy') + + def test_get_magazine_issue_copy(self): + # Does not exist + resp = self.api_call('get', 'magazine-issue-copy/666', {}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine_issue_copy' with id #666 has not been found.", 'code': 1}, resp.json()) + + # Exists + resp = self.api_call('get', 'magazine-issue-copy/1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual({'id': 1, 'magazineIssueId': 1, 'type': 'Paper', 'notes': 'Original'}, resp.json()) + + def test_create_incomplete_payload(self): + resp = self.api_call('post', 'magazine-issue-copy', {}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': 'The following field is missing: magazineIssueId.', 'code': 6}, resp.json()) + + def test_create_unsupported_type(self): + resp = self.api_call('post', 'magazine-issue-copy', {'magazineIssueId': 1, 'type': 'Vinyl'}, True) + + self.assertEqual(400, resp.status_code) + + def test_create_magazine_issue_not_found(self): + resp = self.api_call('post', 'magazine-issue-copy', {'magazineIssueId': 666, 'type': 'Paper'}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) + + def test_create_update_delete_success(self): + # Create + payload = {'magazineIssueId': 1, 'type': 'Digital', 'notes': 'Second digital copy.'} + resp = self.api_call('post', 'magazine-issue-copy', payload, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual('Digital', resp.json()['type']) + copy_id = str(resp.json()['id']) + + resp = self.api_call('get', 'magazine-issue-copy/' + copy_id, None, True) + payload['id'] = 3 + self.assertEqual(payload, resp.json()) + + # Patch + resp = self.api_call('patch', 'magazine-issue-copy/' + copy_id, {'notes': 'Updated note.'}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual('Updated note.', resp.json()['notes']) + + # Delete + resp = self.api_call('delete', 'magazine-issue-copy/' + copy_id, {}, True) + self.assertEqual(200, resp.status_code) + + resp = self.api_call('delete', 'magazine-issue-copy/' + copy_id, {}, True) + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': f"The resource of type 'magazine_issue_copy' with id #{copy_id} has not been found.", 'code': 1}, resp.json()) + + def test_update_not_found(self): + resp = self.api_call('patch', 'magazine-issue-copy/666', {'notes': 'Whatever'}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine_issue_copy' with id #666 has not been found.", 'code': 1}, resp.json()) + + def test_get_list_default_filters(self): + resp = self.api_call('get', 'magazine-issue-copies', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(2, resp.json()['resultCount']) + self.assertEqual(2, resp.json()['totalResultCount']) + self.assertEqual(1, resp.json()['page']) + self.assertEqual(1, resp.json()['totalPageCount']) + + def test_get_list_filter_by_magazine_issue(self): + resp = self.api_call('get', 'magazine-issue-copies?magazineIssueId[]=1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(2, resp.json()['resultCount']) + + resp = self.api_call('get', 'magazine-issue-copies?magazineIssueId[]=2', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(0, resp.json()['resultCount']) diff --git a/test/functional/test_magazine_issues.py b/test/functional/test_magazine_issues.py new file mode 100644 index 0000000..ed64c96 --- /dev/null +++ b/test/functional/test_magazine_issues.py @@ -0,0 +1,114 @@ +from test.abstract_tests import AbstractTests + +class TestMagazineIssues(AbstractTests): + def test_commons(self): + super().check_all_routes_error_bad_user_token('magazine-issue') + super().check_all_routes_error_missing_user_token('magazine-issue') + + def test_get_magazine_issue(self): + # Does not exist + resp = self.api_call('get', 'magazine-issue/666', {}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) + + # Exists + resp = self.api_call('get', 'magazine-issue/1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual({'id': 1, 'magazineId': 1, 'issueNumber': 1, 'year': 1997, 'month': 8, 'notes': 'Le premier !'}, resp.json()) + + def test_create_incomplete_payload(self): + resp = self.api_call('post', 'magazine-issue', {}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': 'The following field is missing: magazineId.', 'code': 6}, resp.json()) + + def test_create_magazine_not_found(self): + resp = self.api_call('post', 'magazine-issue', {'magazineId': 666, 'issueNumber': 1, 'year': 2000, 'month': 1}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine' with id #666 has not been found.", 'code': 1}, resp.json()) + + def test_create_duplicate_issue_number(self): + resp = self.api_call('post', 'magazine-issue', {'magazineId': 1, 'issueNumber': 1, 'year': 1997, 'month': 8}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine_issue' with issue_number '1' already exists.", 'code': 8}, resp.json()) + + def test_create_update_delete_success(self): + # Create + payload = {'magazineId': 1, 'issueNumber': 3, 'year': 1997, 'month': 10, 'notes': 'Le troisième.'} + resp = self.api_call('post', 'magazine-issue', payload, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(3, resp.json()['issueNumber']) + issue_id = str(resp.json()['id']) + + resp = self.api_call('get', 'magazine-issue/' + issue_id, None, True) + payload['id'] = 3 + self.assertEqual(payload, resp.json()) + + # Patch + resp = self.api_call('patch', 'magazine-issue/' + issue_id, {'notes': 'Mis à jour.'}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual('Mis à jour.', resp.json()['notes']) + + # Delete + resp = self.api_call('delete', 'magazine-issue/' + issue_id, {}, True) + self.assertEqual(200, resp.status_code) + + resp = self.api_call('delete', 'magazine-issue/' + issue_id, {}, True) + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': f"The resource of type 'magazine_issue' with id #{issue_id} has not been found.", 'code': 1}, resp.json()) + + def test_update_not_found(self): + resp = self.api_call('patch', 'magazine-issue/666', {'notes': 'Whatever'}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) + + def test_update_duplicate_issue_number(self): + resp = self.api_call('patch', 'magazine-issue/2', {'issueNumber': 1}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine_issue' with issue_number '1' already exists.", 'code': 8}, resp.json()) + + def test_delete_fails_because_issue_has_copies(self): + resp = self.api_call('delete', 'magazine-issue/1', {}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': "The following resource type 'magazine_issue' has children of type 'copy', so it cannot be deleted.", 'code': 9}, resp.json()) + + resp = self.api_call('get', 'magazine-issue/1', {}, True) + self.assertEqual(200, resp.status_code) + + def test_delete_fails_because_issue_has_mentions(self): + resp = self.api_call('delete', 'magazine-issue/2', {}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': "The following resource type 'magazine_issue' has children of type 'game_version_magazine_mention', so it cannot be deleted.", 'code': 9}, resp.json()) + + resp = self.api_call('get', 'magazine-issue/2', {}, True) + self.assertEqual(200, resp.status_code) + + def test_get_list_default_filters(self): + resp = self.api_call('get', 'magazine-issues', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(2, resp.json()['resultCount']) + self.assertEqual(2, resp.json()['totalResultCount']) + self.assertEqual(1, resp.json()['page']) + self.assertEqual(1, resp.json()['totalPageCount']) + + def test_get_list_filter_by_magazine(self): + resp = self.api_call('get', 'magazine-issues?magazineId[]=1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(2, resp.json()['resultCount']) + + resp = self.api_call('get', 'magazine-issues?magazineId[]=2', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(0, resp.json()['resultCount']) diff --git a/test/functional/test_magazines.py b/test/functional/test_magazines.py new file mode 100644 index 0000000..e934ba3 --- /dev/null +++ b/test/functional/test_magazines.py @@ -0,0 +1,104 @@ +from test.abstract_tests import AbstractTests + +class TestMagazines(AbstractTests): + def test_commons(self): + super().check_all_routes_error_bad_user_token('magazine') + super().check_all_routes_error_missing_user_token('magazine') + + def test_get_magazine(self): + # Does not exist + resp = self.api_call('get', 'magazine/666', {}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine' with id #666 has not been found.", 'code': 1}, resp.json()) + + # Exists + resp = self.api_call('get', 'magazine/1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual({'id': 1, 'title': 'Gen4', 'notes': 'Découvert en 1997.', 'issueCount': 2}, resp.json()) + + def test_create_incomplete_payload(self): + resp = self.api_call('post', 'magazine', {}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': 'The following field is missing: title.', 'code': 6}, resp.json()) + + def test_create_duplicate_title(self): + resp = self.api_call('post', 'magazine', {'title': 'Gen4'}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine' with title 'Gen4' already exists.", 'code': 8}, resp.json()) + + def test_create_update_delete_success(self): + # Create + payload = {'title': 'Joystick', 'notes': 'French gaming magazine.'} + resp = self.api_call('post', 'magazine', payload, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual('Joystick', resp.json()['title']) + magazine_id = str(resp.json()['id']) + + resp = self.api_call('get', 'magazine/' + magazine_id, None, True) + payload['id'] = 3 + payload['issueCount'] = 0 # newly created, no issues yet + self.assertEqual(payload, resp.json()) + + # Patch + new_title = 'Joystick II - ' + magazine_id + resp = self.api_call('patch', 'magazine/' + magazine_id, {'title': new_title}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(new_title, resp.json()['title']) + + # Delete + resp = self.api_call('delete', 'magazine/' + magazine_id, {}, True) + self.assertEqual(200, resp.status_code) + + resp = self.api_call('delete', 'magazine/' + magazine_id, {}, True) + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': f"The resource of type 'magazine' with id #{magazine_id} has not been found.", 'code': 1}, resp.json()) + + def test_update_not_found(self): + resp = self.api_call('patch', 'magazine/666', {'title': 'Whatever'}, True) + + self.assertEqual(404, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine' with id #666 has not been found.", 'code': 1}, resp.json()) + + def test_update_duplicate_title(self): + resp = self.api_call('patch', 'magazine/2', {'title': 'Gen4'}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': "The resource of type 'magazine' with title 'PC PLayer' already exists.", 'code': 8}, resp.json()) + + def test_delete_fails_because_magazine_has_issues(self): + resp = self.api_call('delete', 'magazine/1', {}, True) + + self.assertEqual(400, resp.status_code) + self.assertEqual({'message': "The following resource type 'magazine' has children of type 'issue', so it cannot be deleted.", 'code': 9}, resp.json()) + + resp = self.api_call('get', 'magazine/1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual('Gen4', resp.json()['title']) + + def test_get_list_default_filters(self): + resp = self.api_call('get', 'magazines', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(2, resp.json()['resultCount']) + self.assertEqual(2, resp.json()['totalResultCount']) + self.assertEqual(1, resp.json()['page']) + self.assertEqual(1, resp.json()['totalPageCount']) + + def test_get_list_basic_filters(self): + resp = self.api_call('get', 'magazines?page=1&limit=1', {}, True) + + self.assertEqual(200, resp.status_code) + self.assertEqual(1, resp.json()['resultCount']) + self.assertEqual(2, resp.json()['totalResultCount']) + self.assertEqual(1, resp.json()['page']) + self.assertEqual(2, resp.json()['totalPageCount']) + + self.assertEqual(1, resp.json()['result'][0]['id']) + self.assertEqual('Gen4', resp.json()['result'][0]['title']) diff --git a/test/functional/test_notes.py b/test/functional/test_notes.py index 5deb979..1c39f6f 100644 --- a/test/functional/test_notes.py +++ b/test/functional/test_notes.py @@ -45,7 +45,7 @@ def test_create_update_delete_success(self): resp = self.api_call('delete', 'note/' + note_id, {}, True) self.assertEqual(404, resp.status_code) - self.assertEqual({'message': "The resource of type 'note' with id #4 has not been found.", 'code': 1}, resp.json()) + self.assertEqual({'message': "The resource of type 'note' with id #3 has not been found.", 'code': 1}, resp.json()) def test_get_list_basic_filters(self): resp = self.api_call('get', 'notes?page=2&limit=1', {}, True) diff --git a/test/games_test.sql b/test/games_test.sql index 2bc00f6..30ad5d9 100644 --- a/test/games_test.sql +++ b/test/games_test.sql @@ -1,47 +1,16 @@ --- MySQL dump 10.13 Distrib 8.0.32, for Linux (x86_64) --- --- Host: localhost Database: games --- ------------------------------------------------------ --- Server version 8.0.32 +-- Adminer 5.4.2 MySQL 8.0.41-0ubuntu0.20.04.1 dump -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -/*!50503 SET NAMES utf8mb4 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +SET NAMES utf8; +SET time_zone = '+00:00'; +SET foreign_key_checks = 0; +SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; --- --- Table structure for table `copies` --- - -DROP TABLE IF EXISTS `notes`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `notes` ( - `id` smallint unsigned NOT NULL AUTO_INCREMENT, - `title` varchar(255) NOT NULL, - `content` text, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - -LOCK TABLES `notes` WRITE; -/*!40000 ALTER TABLE `notes` DISABLE KEYS */; -INSERT INTO `notes` VALUES (1, 'Note 1','Some comment 1.'),(2, 'Note 2','Some comment 2'); -/*!40000 ALTER TABLE `notes` ENABLE KEYS */; -UNLOCK TABLES; +SET NAMES utf8mb4; DROP TABLE IF EXISTS `copies`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `copies` ( - `copy_id` smallint unsigned NOT NULL AUTO_INCREMENT, - `version_id` smallint unsigned NOT NULL, + `copy_id` int unsigned NOT NULL AUTO_INCREMENT, + `version_id` int unsigned NOT NULL, `is_original` tinyint unsigned NOT NULL, `language` varchar(255) NOT NULL, `box_type` varchar(255) NOT NULL, @@ -58,113 +27,561 @@ CREATE TABLE `copies` ( `comments` text, PRIMARY KEY (`copy_id`), KEY `version_id` (`version_id`), - CONSTRAINT `copies_ibfk_1` FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; + CONSTRAINT `copies_ibfk_1` FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --- --- Dumping data for table `copies` --- +INSERT INTO `copies` (`copy_id`, `version_id`, `is_original`, `language`, `box_type`, `is_box_repro`, `casing_type`, `support_type`, `on_compilation`, `is_reedition`, `has_manual`, `status`, `type`, `region`, `is_rom`, `comments`) VALUES +(1, 348, 1, 'fr', 'Big box', 0, 'CD-like', 'CD-ROM', 0, 0, 1, 'In', 'Physical', 'PAL', 0, 'Bought it in 2004'), +(2, 349, 1, 'fr', 'none', 0, 'Cardboard sleeve', 'CD-ROM', 1, 1, 0, 'In', 'Physical', 'PAL', 0, 'Got it with my cereals'), +(3, 245, 1, 'fr', 'None', 0, 'CD-like', 'CD-ROM', 1, 1, 0, 'In', 'Physical', 'PAL', 0, 'pues'); -LOCK TABLES `copies` WRITE; -/*!40000 ALTER TABLE `copies` DISABLE KEYS */; -INSERT INTO `copies` VALUES (1,348,1,'fr','Big box',0,'CD-like','CD-ROM',0,0,1,'In','Physical','PAL',0,'Bought it in 2004'),(2,349,1,'fr','none',0,'Cardboard sleeve','CD-ROM',1,1,0,'In','Physical','PAL',0,'Got it with my cereals'),(3,245,1,'fr','None',0,'CD-like','CD-ROM',1,1,0,'In','Physical','PAL',0,'pues'); -/*!40000 ALTER TABLE `copies` ENABLE KEYS */; -UNLOCK TABLES; +DROP TABLE IF EXISTS `game_version_magazine_mentions`; +CREATE TABLE `game_version_magazine_mentions` ( + `mention_id` int unsigned NOT NULL AUTO_INCREMENT, + `magazine_issue_id` int unsigned NOT NULL, + `game_version_id` int unsigned NOT NULL, + `type` varchar(255) NOT NULL, + `notes` text NOT NULL, + PRIMARY KEY (`mention_id`), + KEY `fk_mention_magazine_issue` (`magazine_issue_id`), + KEY `fk_mention_game_version` (`game_version_id`), + CONSTRAINT `fk_mention_game_version` FOREIGN KEY (`game_version_id`) REFERENCES `versions` (`version_id`), + CONSTRAINT `fk_mention_magazine_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --- --- Table structure for table `games` --- +INSERT INTO `game_version_magazine_mentions` (`mention_id`, `magazine_issue_id`, `game_version_id`, `type`, `notes`) VALUES +(1, 1, 1, 'Test', ''), +(2, 2, 1, 'Guide', ''); DROP TABLE IF EXISTS `games`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `games` ( - `id` smallint unsigned NOT NULL AUTO_INCREMENT, + `id` int unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL DEFAULT '', `notes` text, PRIMARY KEY (`id`), UNIQUE KEY `game_title` (`title`), KEY `title` (`title`) -) ENGINE=InnoDB AUTO_INCREMENT=381 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +INSERT INTO `games` (`id`, `title`, `notes`) VALUES +(1, 'Sega Soccer', 'super jeu !!!'), +(2, 'Columns', 'bof bof'), +(3, 'Super Monaco GP', NULL), +(4, 'Revenge Of Shinobi', NULL), +(5, 'Sonic', NULL), +(6, 'Streets of Rage', NULL), +(7, 'Fifa Soccer 96', NULL), +(8, 'Tomb Raider', NULL), +(9, 'ClockWork Knight', NULL), +(10, 'Firestorm Thunderhawk 2', NULL), +(11, 'Sega Rally', NULL), +(13, 'Daytona USA', NULL), +(14, 'Bug', NULL), +(15, 'Worlwide Soccer 97', NULL), +(16, 'Virtua Cop 2', NULL), +(17, 'Driver', NULL), +(18, 'Driver 2', NULL), +(19, 'Die Hard Trilogy', NULL), +(20, 'Gran Turismo II', NULL), +(21, 'Resident Evil', NULL), +(22, 'Resident Evil 2', NULL), +(23, 'Resident Evil 3', NULL), +(24, 'Medievil', NULL), +(25, 'Metal Gear Solid', NULL), +(26, 'Silent Hill', NULL), +(27, 'Fifa 98', NULL), +(28, 'Les Razmokets', NULL), +(29, 'Fifa 97', NULL), +(30, 'V-Rally 97 Championship Edition', NULL), +(31, 'Crash Bandicoot 3', NULL), +(32, 'Medal Of Honor Resistance', NULL), +(33, 'Medal Of Honor', NULL), +(34, 'Ridge Racer Type 4', NULL), +(35, 'Wing Over', NULL), +(36, 'Sherif fais moi peur', NULL), +(37, '007 Le monde ne suffit pas', NULL), +(38, 'Little Big Adventure', NULL), +(39, 'Tekken 3', NULL), +(40, 'Goldeneye', NULL), +(41, 'Zelda Ocarina Of Time', NULL), +(42, 'Zelda Majora\'s Mask', NULL), +(43, '1080° Snowboarding', NULL), +(44, 'World Cup 98', NULL), +(45, 'Mario 64', NULL), +(46, 'Mario Golf', NULL), +(47, 'Mario Tennis', NULL), +(48, 'Mario Kart 64', NULL), +(49, 'Yoshi\'s Story', NULL), +(50, 'Sonic Adventure', NULL), +(51, 'V-Rally 2 Expert Edition', NULL), +(52, 'Confict Zone', NULL), +(53, 'Speed Devils', NULL), +(54, 'Evolution the world of sacred device', NULL), +(56, 'Trick Style', NULL), +(58, 'Resident Evil Code Veronica', NULL), +(59, 'F355 Challenge', NULL), +(60, 'Vigilant 8 2nd Offense', NULL), +(61, 'Tekken Tag', NULL), +(62, 'Crash Bandicoot: La vengeance de Cortex', NULL), +(64, 'Pro Evolution Soccer 2', NULL), +(68, 'Silent Hill 4', NULL), +(69, 'Le monde des bleus 2002', NULL), +(70, 'Silent Hill 3', NULL), +(71, 'Silent Hill 2', NULL), +(72, 'Tekken 5', NULL), +(73, 'Silent Hill Origins', NULL), +(74, 'Resident Evil 0', NULL), +(75, 'Zelda WindWaker', NULL), +(76, 'Mario Party 5', NULL), +(78, 'NBA Courtside 2002', NULL), +(79, 'Eternal Darkness', NULL), +(80, 'Super Smash Bros Melee', NULL), +(81, 'Mario Kart Double Dash', NULL), +(82, 'Zelda Twilight Princess', NULL), +(83, 'Donkey Konga 2', NULL), +(84, 'Resident Evil Umbrella Chronicles', NULL), +(85, 'Mario Kart Wii', NULL), +(86, 'Les lapins crétins: retour vers le passé', NULL), +(87, 'Super Mario Bros Wii', NULL), +(88, 'Resident Evil The Dark Side Chronicle', NULL), +(89, 'Disaster day of crisis', NULL), +(90, 'The Dead Rising', NULL), +(91, 'Wii Sports', NULL), +(92, 'Super Smash Bros Brawl', NULL), +(93, 'Epic Mickey', NULL), +(94, 'Donkey Kong Country Returns', NULL), +(95, 'Band Hero', NULL), +(96, 'Titanic: An Adventure Out Of Time', NULL), +(97, 'Démonts et Manants', NULL), +(98, 'Silent Hunter II', NULL), +(99, 'Starcraft Brood War', NULL), +(100, 'IL2 Sturmovik', NULL), +(101, 'The FullThrottle', NULL), +(102, 'Gabriel Knight II: The Beast Whithin', NULL), +(104, 'Medal Of Honor l\'Offensive', NULL), +(105, 'Pirates', NULL), +(106, 'The Settlers II - Tenth anniversary edition', NULL), +(107, 'Age of Empires II', NULL), +(108, 'Age Of Empires II The Conquerors', NULL), +(110, 'Commandos II', NULL), +(111, 'Pro Evolution Soccer 6', NULL), +(112, 'Iznogoud', NULL), +(113, 'Rise Of Nations Throne & Patriots', NULL), +(114, 'Commandos: Derrière les lignes ennemies', NULL), +(115, 'Commandos: Le sens du devoir', NULL), +(116, 'SPQR', NULL), +(117, 'Frankeinstein The Eyes of the Monster', NULL), +(119, 'Carmageddon', NULL), +(120, 'Carmageddon 2', NULL), +(121, 'Starcraft', NULL), +(122, 'Tomb Raider II', NULL), +(123, 'Marine Malice et le Mystère des graines d\'algues', NULL), +(124, 'Worms Fort Etat de Siège', NULL), +(125, 'F22 Lighting II', NULL), +(126, 'Combat Flight Simulator', NULL), +(127, 'Red Baron 3D', NULL), +(128, 'Rise of Nations ', NULL), +(129, 'Medal Of Honor Spearhead', NULL), +(130, 'Little Big Adventure 2', NULL), +(132, 'Aces over Europe', NULL), +(133, 'Aces of the Pacific', NULL), +(134, 'Red Baron', NULL), +(135, 'A-10 Tank Killer', NULL), +(136, 'The aviation pionneers', NULL), +(137, 'Command Aces of the Deep', NULL), +(138, 'A10-II Silent Thunder', NULL), +(139, 'Monkey Island IV', NULL), +(140, 'Monkey Island III', NULL), +(141, 'Egypte 1', NULL), +(142, 'Power Chess', NULL), +(143, 'Worms Armagueddon', NULL), +(144, 'Le bouclier de Quetzacoatl (Broken sword II)', NULL), +(145, 'Les chevaliers de Baphomet (Broken Sword 1)', NULL), +(146, '688 Sumarine', NULL), +(147, 'Aces High', NULL), +(148, 'Destroyer Simulation', NULL), +(149, 'Warbird 2', NULL), +(150, 'Caesar 2', NULL), +(152, 'Dreadnough', NULL), +(153, 'Half Life', NULL), +(154, 'Holiday Island', NULL), +(155, 'Liverpool FC', NULL), +(156, 'Monkey Island II', NULL), +(157, 'Project IGI', NULL), +(158, 'Transport Tycoon Deluxe', NULL), +(159, 'Tomb Raider IV', NULL), +(160, 'The Last Express', NULL), +(161, 'Age Of Empires III', NULL), +(162, 'Age Of Empires III Warchiefs', NULL), +(163, 'Age Of Empires III Asian Dynasties', NULL), +(164, 'Grim Fandango', NULL), +(165, 'Phantasmagoria', NULL), +(166, 'Hitman', NULL), +(167, 'Metal Rage', NULL), +(168, 'Virtua Tennis 3', NULL), +(169, 'Sega Rally 2', NULL), +(170, 'Dust A tale in the wired west', NULL), +(171, 'Football Manager 96/97', NULL), +(172, 'Le trésor du San Diego', NULL), +(174, 'Versailles II', NULL), +(175, 'The X Files', NULL), +(176, 'Titanic un voyage interactif', NULL), +(177, 'Crazy Taxi', NULL), +(178, 'Tout le bridge aujourd\'hui', NULL), +(179, 'Dracula 2', NULL), +(180, 'Constructor', NULL), +(181, 'Morrowind', NULL), +(182, 'Splinter Cell', NULL), +(183, 'Virtua Tennis', NULL), +(184, 'Mafia 2', NULL), +(185, 'Silent Hunter IV', NULL), +(186, 'Black And White 2', NULL), +(187, 'Medal Of Honor Allied Assault', NULL), +(188, 'Versailles', NULL), +(189, 'Un voisin d\'enfer', NULL), +(190, 'Alice Madness Returns', NULL), +(191, 'Silent Hunter III', NULL), +(192, 'Star Wars Knights Of The Old Republic', NULL), +(193, 'Syberia', NULL), +(194, 'Virtual Pool', NULL), +(195, 'Descent', NULL), +(196, 'Need For Speed V', NULL), +(197, 'Need For Speed III', NULL), +(198, 'Les Visiteurs: la relique de Sainte Rolande', NULL), +(199, 'Les cochons de guerre', NULL), +(200, 'Age of Empires Gold', NULL), +(201, 'Discworld', NULL), +(202, 'Discworld 2', NULL), +(203, 'Lemmings Revolution', NULL), +(204, 'Screamer 4x4 Rally', NULL), +(205, 'Theme Hospital', NULL), +(206, 'Woodruff', NULL), +(207, 'Mafia', NULL), +(208, 'Fighting Steel', NULL), +(209, 'Oblivion', NULL), +(210, 'Tribunal (ext. Morrowind)', NULL), +(211, 'Syberia 2', NULL), +(213, 'Shivers', NULL), +(214, 'Desperados I : Wanted Dead or Alive', NULL), +(215, 'Runaway', NULL), +(217, 'Trine 2', NULL), +(218, 'Trine 1', NULL), +(219, 'Beetle Crazy Cup', NULL), +(220, 'Tomb Raider III', NULL), +(222, 'Football Manager 98/99', NULL), +(223, 'Simcity 2000', NULL), +(224, 'Conker\'s Bad Fur Day', NULL), +(225, 'Myst', NULL), +(229, 'Runaway 2', NULL), +(230, 'Runaway 3 ', NULL), +(231, 'Syberia 3', NULL), +(232, 'Daria', NULL), +(233, 'Football Manager 2005', NULL), +(234, 'Gobliiins', NULL), +(236, 'Midtown Madness', NULL), +(237, 'Midtown Madness 2', NULL), +(239, 'Ecstatica 2', NULL), +(240, 'Pandemonium', NULL), +(241, 'Pandemonium 2', NULL), +(243, 'L\'Amerzone', NULL), +(245, 'Leisure Suit Larry I: in the Land of the Lounge Lizards', NULL), +(246, 'The dig', NULL), +(247, 'Simon the sorcerer', NULL), +(248, 'Yesterday Origins', NULL), +(249, 'The day of the tentacle', NULL), +(250, 'Nibiru : age of secrets', NULL), +(251, 'Gabriel Knight I: Sins of The Father', NULL), +(252, 'Super Mario Land 2', NULL), +(253, 'Bloodmoon (Extension Morrowind)', NULL), +(255, 'Black and White', NULL), +(256, 'Phantasmagoria : obsessions fatales', NULL), +(257, 'Alone in the dark', NULL), +(258, 'Combat Flight Simulator 2', NULL), +(259, 'Dino Crisis 2', NULL), +(260, 'Thimbleweed Park', NULL), +(261, 'Sam and Max', NULL), +(262, 'Indiana Jones and the fate of Atlantis', NULL), +(263, 'Paradise', NULL), +(264, 'Ace Ventura', NULL), +(265, 'Kursk', NULL), +(267, 'Gabriel Knight III: Blood of the Sacred, Blood of the Damned', NULL), +(269, 'Back to the Future: The Game', NULL), +(270, 'Atlantis', NULL), +(271, 'Dino Crisis', NULL), +(273, 'Chicago 1930', NULL), +(274, 'Warcraft Adventures', NULL), +(275, 'Fallout', NULL), +(276, 'Dune', NULL), +(277, 'Loom', NULL), +(279, 'Another World', NULL), +(281, 'Gobliins 2: The Prince Buffoon', NULL), +(282, 'Goblins Quest 3', NULL), +(283, 'Indiana Jones and the last crusade', NULL), +(284, 'Blade Runner', NULL), +(285, 'Warcraft II', NULL), +(286, 'Pompei', NULL), +(287, 'Tomb Raider V', NULL), +(288, 'Myst II : Riven', NULL), +(289, 'Myst III - Exile', NULL), +(290, 'Dracula', NULL), +(291, 'Leisure Suit Larry II: Goes Looking for Love (in Several Wrong Places)', NULL), +(292, 'Leisure Suit Larry III: Passionate Patti in Pursuit of the Pulsating Pectorals', NULL), +(293, 'Leisure Suit Larry V: Passionate Patti Does a Little Undercover Work', NULL), +(294, 'Leisure Suit Larry VI: Shape Up or Slip Out!', NULL), +(295, 'Leisure Suit Larry VII: Love for Sail!', NULL), +(296, 'Leisure Suit Larry: Magna Cum Laude', NULL), +(297, 'Leisure Suit Larry: Box Office Bust', NULL), +(298, 'Leisure Suit Larry: Reloaded', NULL), +(299, 'Leisure Suit Larry: Wet Dreams Don\'t Dry', NULL), +(300, 'Pro Evolution Soccer', NULL), +(301, 'Vigilante 8', NULL), +(304, 'Rayman contre les lapins encore + crétins', NULL), +(305, 'Jack in the Dark', NULL), +(307, 'Alone in the Dark 3 ', NULL), +(308, 'Alone in the Dark: The New Nightmare', NULL), +(309, 'Les Visiteurs : le jeu', NULL), +(310, 'Dark Earth', NULL), +(311, 'Star Wars racer', NULL), +(312, 'Desperados II: Western Commandos : La Revanche de Cooper', NULL), +(313, 'Desperados: Helldorado', NULL), +(314, 'Desperados III', NULL), +(315, 'Max Payne', NULL), +(316, 'Panzer General II', NULL), +(319, 'Myst IV: Revelation', NULL), +(320, 'Uru: ages beyond Myst', NULL), +(321, 'Rule the waves II', NULL), +(322, 'Caesar 3', NULL), +(323, 'Pharaon', NULL), +(325, 'Broken Sword 3: The Sleeping Dragon', NULL), +(326, 'Broken Sword 4: The Angel of Death', NULL), +(327, 'Broken Sword 5: The Serpent\'s Curse', NULL), +(328, 'Tales of Monkey Island: chapter 1', NULL), +(329, 'Tales of Monkey Island: chapter 2 The siege of spinner cay', NULL), +(330, 'Tales of Monkey Island: chapter 3: lair of the leviathan', NULL), +(331, 'Tales of Monkey Island: chapter 4: the trial and execution of Guybrush Threepwood', NULL), +(332, 'Tales of Monkey Island: chapter 5: rise of the pirate god', NULL), +(333, 'Jack and Daxter', NULL), +(334, 'Lost Horizon', NULL), +(335, 'Paris 1313 le disparu de notre dame', NULL), +(337, 'Egypte II', NULL), +(338, 'Chine intrigue dans la cité interdite', NULL), +(339, 'American McGee\'s Alice', NULL), +(340, 'WWII Online', NULL), +(341, 'Rayman 2: The Great Escape', NULL), +(342, 'Star Wars: Episode I Battle for Naboo', NULL), +(343, 'Space Station Silicon Valley', NULL), +(344, 'Indiana Jones et la Machine Infernale', NULL), +(345, 'Parasite Eve', NULL), +(346, 'The Legend of Zelda: A Link to the Past', NULL), +(347, 'Soccer', NULL), +(348, 'Fifa 2000', NULL), +(349, 'Sonic R', NULL), +(350, 'Luigi\'s Mansion', NULL), +(351, 'Atlantis II', NULL), +(352, 'Fifa 11', NULL), +(354, 'Alone in the Dark 2: One eyed Jack\'s Revenge', NULL), +(355, 'Pilot Wings', NULL), +(356, 'Beetle Adventures Racing', NULL), +(357, 'Yoshi\'s Island', NULL), +(358, 'Donkey Kong Country', NULL), +(359, 'Donkey Kong 64', NULL), +(360, 'The Settlers II', NULL), +(361, 'Discworld noir', NULL), +(362, 'Beavis and Butt-Head in Virtual Stupidity', NULL), +(363, 'Deus ex', NULL), +(364, 'Rayman 3: Hoodlum Havoc', NULL), +(366, 'Swat 2', NULL), +(367, 'Quest for Glory: Shadows of Darkness', NULL), +(368, 'Simcity 3000', NULL), +(369, 'Faust', NULL), +(370, 'Pilgrim', NULL), +(371, 'Aztec', NULL), +(372, 'Cossacks', NULL), +(373, 'Anno 1602', NULL), +(374, 'Panzer Commander', NULL), +(375, 'Croc: legend of the gobbos', NULL), +(376, 'Fifa 99', NULL), +(377, 'Destruction Derby', NULL), +(378, 'Soviet Strike', NULL), +(379, 'Tonic Trouble', NULL), +(380, 'Flight Simulator 2000', NULL); + +DROP TABLE IF EXISTS `magazine_issue_copies`; +CREATE TABLE `magazine_issue_copies` ( + `issue_copy_id` int unsigned NOT NULL AUTO_INCREMENT, + `magazine_issue_id` int unsigned NOT NULL, + `type` varchar(255) NOT NULL, + `notes` text NOT NULL, + PRIMARY KEY (`issue_copy_id`), + KEY `fk_magazine_issue_copy_issue` (`magazine_issue_id`), + CONSTRAINT `fk_magazine_issue_copy_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +INSERT INTO `magazine_issue_copies` (`issue_copy_id`, `magazine_issue_id`, `type`, `notes`) VALUES +(1, 1, 'Paper', 'Original'), +(2, 1, 'Digital', 'Copy.'); + +DROP TABLE IF EXISTS `magazine_issues`; +CREATE TABLE `magazine_issues` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `magazine_id` int unsigned NOT NULL, + `issue_number` smallint unsigned NOT NULL, + `year` smallint unsigned NOT NULL, + `month` tinyint unsigned NOT NULL, + `notes` text NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_magazine_issue_magazine` (`magazine_id`), + CONSTRAINT `fk_magazine_issue_magazine` FOREIGN KEY (`magazine_id`) REFERENCES `magazines` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +INSERT INTO `magazine_issues` (`id`, `magazine_id`, `issue_number`, `year`, `month`, `notes`) VALUES +(1, 1, 1, 1997, 8, 'Le premier !'), +(2, 1, 2, 1997, 9, 'Le second.'); --- --- Dumping data for table `games` --- +DROP TABLE IF EXISTS `magazines`; +CREATE TABLE `magazines` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `title` text NOT NULL, + `notes` text NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +INSERT INTO `magazines` (`id`, `title`, `notes`) VALUES +(1, 'Gen4', 'Découvert en 1997.'), +(2, 'PC PLayer', 'Découvert début 1998.'); -LOCK TABLES `games` WRITE; -/*!40000 ALTER TABLE `games` DISABLE KEYS */; -INSERT INTO `games` VALUES (1,'Sega Soccer','super jeu !!!'),(2,'Columns','bof bof'),(3,'Super Monaco GP',NULL),(4,'Revenge Of Shinobi',NULL),(5,'Sonic',NULL),(6,'Streets of Rage',NULL),(7,'Fifa Soccer 96',NULL),(8,'Tomb Raider',NULL),(9,'ClockWork Knight',NULL),(10,'Firestorm Thunderhawk 2',NULL),(11,'Sega Rally',NULL),(13,'Daytona USA',NULL),(14,'Bug',NULL),(15,'Worlwide Soccer 97',NULL),(16,'Virtua Cop 2',NULL),(17,'Driver',NULL),(18,'Driver 2',NULL),(19,'Die Hard Trilogy',NULL),(20,'Gran Turismo II',NULL),(21,'Resident Evil',NULL),(22,'Resident Evil 2',NULL),(23,'Resident Evil 3',NULL),(24,'Medievil',NULL),(25,'Metal Gear Solid',NULL),(26,'Silent Hill',NULL),(27,'Fifa 98',NULL),(28,'Les Razmokets',NULL),(29,'Fifa 97',NULL),(30,'V-Rally 97 Championship Edition',NULL),(31,'Crash Bandicoot 3',NULL),(32,'Medal Of Honor Resistance',NULL),(33,'Medal Of Honor',NULL),(34,'Ridge Racer Type 4',NULL),(35,'Wing Over',NULL),(36,'Sherif fais moi peur',NULL),(37,'007 Le monde ne suffit pas',NULL),(38,'Little Big Adventure',NULL),(39,'Tekken 3',NULL),(40,'Goldeneye',NULL),(41,'Zelda Ocarina Of Time',NULL),(42,'Zelda Majora\'s Mask',NULL),(43,'1080° Snowboarding',NULL),(44,'World Cup 98',NULL),(45,'Mario 64',NULL),(46,'Mario Golf',NULL),(47,'Mario Tennis',NULL),(48,'Mario Kart 64',NULL),(49,'Yoshi\'s Story',NULL),(50,'Sonic Adventure',NULL),(51,'V-Rally 2 Expert Edition',NULL),(52,'Confict Zone',NULL),(53,'Speed Devils',NULL),(54,'Evolution the world of sacred device',NULL),(56,'Trick Style',NULL),(58,'Resident Evil Code Veronica',NULL),(59,'F355 Challenge',NULL),(60,'Vigilant 8 2nd Offense',NULL),(61,'Tekken Tag',NULL),(62,'Crash Bandicoot: La vengeance de Cortex',NULL),(64,'Pro Evolution Soccer 2',NULL),(68,'Silent Hill 4',NULL),(69,'Le monde des bleus 2002',NULL),(70,'Silent Hill 3',NULL),(71,'Silent Hill 2',NULL),(72,'Tekken 5',NULL),(73,'Silent Hill Origins',NULL),(74,'Resident Evil 0',NULL),(75,'Zelda WindWaker',NULL),(76,'Mario Party 5',NULL),(78,'NBA Courtside 2002',NULL),(79,'Eternal Darkness',NULL),(80,'Super Smash Bros Melee',NULL),(81,'Mario Kart Double Dash',NULL),(82,'Zelda Twilight Princess',NULL),(83,'Donkey Konga 2',NULL),(84,'Resident Evil Umbrella Chronicles',NULL),(85,'Mario Kart Wii',NULL),(86,'Les lapins crétins: retour vers le passé',NULL),(87,'Super Mario Bros Wii',NULL),(88,'Resident Evil The Dark Side Chronicle',NULL),(89,'Disaster day of crisis',NULL),(90,'The Dead Rising',NULL),(91,'Wii Sports',NULL),(92,'Super Smash Bros Brawl',NULL),(93,'Epic Mickey',NULL),(94,'Donkey Kong Country Returns',NULL),(95,'Band Hero',NULL),(96,'Titanic: An Adventure Out Of Time',NULL),(97,'Démonts et Manants',NULL),(98,'Silent Hunter II',NULL),(99,'Starcraft Brood War',NULL),(100,'IL2 Sturmovik',NULL),(101,'The FullThrottle',NULL),(102,'Gabriel Knight II: The Beast Whithin',NULL),(104,'Medal Of Honor l\'Offensive',NULL),(105,'Pirates',NULL),(106,'The Settlers II - Tenth anniversary edition',NULL),(107,'Age of Empires II',NULL),(108,'Age Of Empires II The Conquerors',NULL),(110,'Commandos II',NULL),(111,'Pro Evolution Soccer 6',NULL),(112,'Iznogoud',NULL),(113,'Rise Of Nations Throne & Patriots',NULL),(114,'Commandos: Derrière les lignes ennemies',NULL),(115,'Commandos: Le sens du devoir',NULL),(116,'SPQR',NULL),(117,'Frankeinstein The Eyes of the Monster',NULL),(119,'Carmageddon',NULL),(120,'Carmageddon 2',NULL),(121,'Starcraft',NULL),(122,'Tomb Raider II',NULL),(123,'Marine Malice et le Mystère des graines d\'algues',NULL),(124,'Worms Fort Etat de Siège',NULL),(125,'F22 Lighting II',NULL),(126,'Combat Flight Simulator',NULL),(127,'Red Baron 3D',NULL),(128,'Rise of Nations ',NULL),(129,'Medal Of Honor Spearhead',NULL),(130,'Little Big Adventure 2',NULL),(132,'Aces over Europe',NULL),(133,'Aces of the Pacific',NULL),(134,'Red Baron',NULL),(135,'A-10 Tank Killer',NULL),(136,'The aviation pionneers',NULL),(137,'Command Aces of the Deep',NULL),(138,'A10-II Silent Thunder',NULL),(139,'Monkey Island IV',NULL),(140,'Monkey Island III',NULL),(141,'Egypte 1',NULL),(142,'Power Chess',NULL),(143,'Worms Armagueddon',NULL),(144,'Le bouclier de Quetzacoatl (Broken sword II)',NULL),(145,'Les chevaliers de Baphomet (Broken Sword 1)',NULL),(146,'688 Sumarine',NULL),(147,'Aces High',NULL),(148,'Destroyer Simulation',NULL),(149,'Warbird 2',NULL),(150,'Caesar 2',NULL),(152,'Dreadnough',NULL),(153,'Half Life',NULL),(154,'Holiday Island',NULL),(155,'Liverpool FC',NULL),(156,'Monkey Island II',NULL),(157,'Project IGI',NULL),(158,'Transport Tycoon Deluxe',NULL),(159,'Tomb Raider IV',NULL),(160,'The Last Express',NULL),(161,'Age Of Empires III',NULL),(162,'Age Of Empires III Warchiefs',NULL),(163,'Age Of Empires III Asian Dynasties',NULL),(164,'Grim Fandango',NULL),(165,'Phantasmagoria',NULL),(166,'Hitman',NULL),(167,'Metal Rage',NULL),(168,'Virtua Tennis 3',NULL),(169,'Sega Rally 2',NULL),(170,'Dust A tale in the wired west',NULL),(171,'Football Manager 96/97',NULL),(172,'Le trésor du San Diego',NULL),(174,'Versailles II',NULL),(175,'The X Files',NULL),(176,'Titanic un voyage interactif',NULL),(177,'Crazy Taxi',NULL),(178,'Tout le bridge aujourd\'hui',NULL),(179,'Dracula 2',NULL),(180,'Constructor',NULL),(181,'Morrowind',NULL),(182,'Splinter Cell',NULL),(183,'Virtua Tennis',NULL),(184,'Mafia 2',NULL),(185,'Silent Hunter IV',NULL),(186,'Black And White 2',NULL),(187,'Medal Of Honor Allied Assault',NULL),(188,'Versailles',NULL),(189,'Un voisin d\'enfer',NULL),(190,'Alice Madness Returns',NULL),(191,'Silent Hunter III',NULL),(192,'Star Wars Knights Of The Old Republic',NULL),(193,'Syberia',NULL),(194,'Virtual Pool',NULL),(195,'Descent',NULL),(196,'Need For Speed V',NULL),(197,'Need For Speed III',NULL),(198,'Les Visiteurs: la relique de Sainte Rolande',NULL),(199,'Les cochons de guerre',NULL),(200,'Age of Empires Gold',NULL),(201,'Discworld',NULL),(202,'Discworld 2',NULL),(203,'Lemmings Revolution',NULL),(204,'Screamer 4x4 Rally',NULL),(205,'Theme Hospital',NULL),(206,'Woodruff',NULL),(207,'Mafia',NULL),(208,'Fighting Steel',NULL),(209,'Oblivion',NULL),(210,'Tribunal (ext. Morrowind)',NULL),(211,'Syberia 2',NULL),(213,'Shivers',NULL),(214,'Desperados I : Wanted Dead or Alive',NULL),(215,'Runaway',NULL),(217,'Trine 2',NULL),(218,'Trine 1',NULL),(219,'Beetle Crazy Cup',NULL),(220,'Tomb Raider III',NULL),(222,'Football Manager 98/99',NULL),(223,'Simcity 2000',NULL),(224,'Conker\'s Bad Fur Day',NULL),(225,'Myst',NULL),(229,'Runaway 2',NULL),(230,'Runaway 3 ',NULL),(231,'Syberia 3',NULL),(232,'Daria',NULL),(233,'Football Manager 2005',NULL),(234,'Gobliiins',NULL),(236,'Midtown Madness',NULL),(237,'Midtown Madness 2',NULL),(239,'Ecstatica 2',NULL),(240,'Pandemonium',NULL),(241,'Pandemonium 2',NULL),(243,'L\'Amerzone',NULL),(245,'Leisure Suit Larry I: in the Land of the Lounge Lizards',NULL),(246,'The dig',NULL),(247,'Simon the sorcerer',NULL),(248,'Yesterday Origins',NULL),(249,'The day of the tentacle',NULL),(250,'Nibiru : age of secrets',NULL),(251,'Gabriel Knight I: Sins of The Father',NULL),(252,'Super Mario Land 2',NULL),(253,'Bloodmoon (Extension Morrowind)',NULL),(255,'Black and White',NULL),(256,'Phantasmagoria : obsessions fatales',NULL),(257,'Alone in the dark',NULL),(258,'Combat Flight Simulator 2',NULL),(259,'Dino Crisis 2',NULL),(260,'Thimbleweed Park',NULL),(261,'Sam and Max',NULL),(262,'Indiana Jones and the fate of Atlantis',NULL),(263,'Paradise',NULL),(264,'Ace Ventura',NULL),(265,'Kursk',NULL),(267,'Gabriel Knight III: Blood of the Sacred, Blood of the Damned',NULL),(269,'Back to the Future: The Game',NULL),(270,'Atlantis',NULL),(271,'Dino Crisis',NULL),(273,'Chicago 1930',NULL),(274,'Warcraft Adventures',NULL),(275,'Fallout',NULL),(276,'Dune',NULL),(277,'Loom',NULL),(279,'Another World',NULL),(281,'Gobliins 2: The Prince Buffoon',NULL),(282,'Goblins Quest 3',NULL),(283,'Indiana Jones and the last crusade',NULL),(284,'Blade Runner',NULL),(285,'Warcraft II',NULL),(286,'Pompei',NULL),(287,'Tomb Raider V',NULL),(288,'Myst II : Riven',NULL),(289,'Myst III - Exile',NULL),(290,'Dracula',NULL),(291,'Leisure Suit Larry II: Goes Looking for Love (in Several Wrong Places)',NULL),(292,'Leisure Suit Larry III: Passionate Patti in Pursuit of the Pulsating Pectorals',NULL),(293,'Leisure Suit Larry V: Passionate Patti Does a Little Undercover Work',NULL),(294,'Leisure Suit Larry VI: Shape Up or Slip Out!',NULL),(295,'Leisure Suit Larry VII: Love for Sail!',NULL),(296,'Leisure Suit Larry: Magna Cum Laude',NULL),(297,'Leisure Suit Larry: Box Office Bust',NULL),(298,'Leisure Suit Larry: Reloaded',NULL),(299,'Leisure Suit Larry: Wet Dreams Don\'t Dry',NULL),(300,'Pro Evolution Soccer',NULL),(301,'Vigilante 8',NULL),(304,'Rayman contre les lapins encore + crétins',NULL),(305,'Jack in the Dark',NULL),(307,'Alone in the Dark 3 ',NULL),(308,'Alone in the Dark: The New Nightmare',NULL),(309,'Les Visiteurs : le jeu',NULL),(310,'Dark Earth',NULL),(311,'Star Wars racer',NULL),(312,'Desperados II: Western Commandos : La Revanche de Cooper',NULL),(313,'Desperados: Helldorado',NULL),(314,'Desperados III',NULL),(315,'Max Payne',NULL),(316,'Panzer General II',NULL),(319,'Myst IV: Revelation',NULL),(320,'Uru: ages beyond Myst',NULL),(321,'Rule the waves II',NULL),(322,'Caesar 3',NULL),(323,'Pharaon',NULL),(325,'Broken Sword 3: The Sleeping Dragon',NULL),(326,'Broken Sword 4: The Angel of Death',NULL),(327,'Broken Sword 5: The Serpent\'s Curse',NULL),(328,'Tales of Monkey Island: chapter 1',NULL),(329,'Tales of Monkey Island: chapter 2 The siege of spinner cay',NULL),(330,'Tales of Monkey Island: chapter 3: lair of the leviathan',NULL),(331,'Tales of Monkey Island: chapter 4: the trial and execution of Guybrush Threepwood',NULL),(332,'Tales of Monkey Island: chapter 5: rise of the pirate god',NULL),(333,'Jack and Daxter',NULL),(334,'Lost Horizon',NULL),(335,'Paris 1313 le disparu de notre dame',NULL),(337,'Egypte II',NULL),(338,'Chine intrigue dans la cité interdite',NULL),(339,'American McGee\'s Alice',NULL),(340,'WWII Online',NULL),(341,'Rayman 2: The Great Escape',NULL),(342,'Star Wars: Episode I Battle for Naboo',NULL),(343,'Space Station Silicon Valley',NULL),(344,'Indiana Jones et la Machine Infernale',NULL),(345,'Parasite Eve',NULL),(346,'The Legend of Zelda: A Link to the Past',NULL),(347,'Soccer',NULL),(348,'Fifa 2000',NULL),(349,'Sonic R',NULL),(350,'Luigi\'s Mansion',NULL),(351,'Atlantis II',NULL),(352,'Fifa 11',NULL),(354,'Alone in the Dark 2: One eyed Jack\'s Revenge',NULL),(355,'Pilot Wings',NULL),(356,'Beetle Adventures Racing',NULL),(357,'Yoshi\'s Island',NULL),(358,'Donkey Kong Country',NULL),(359,'Donkey Kong 64',NULL),(360,'The Settlers II',NULL),(361,'Discworld noir',NULL),(362,'Beavis and Butt-Head in Virtual Stupidity',NULL),(363,'Deus ex',NULL),(364,'Rayman 3: Hoodlum Havoc',NULL),(366,'Swat 2',NULL),(367,'Quest for Glory: Shadows of Darkness',NULL),(368,'Simcity 3000',NULL),(369,'Faust',NULL),(370,'Pilgrim',NULL),(371,'Aztec',NULL),(372,'Cossacks',NULL),(373,'Anno 1602',NULL),(374,'Panzer Commander',NULL),(375,'Croc: legend of the gobbos',NULL),(376,'Fifa 99',NULL),(377,'Destruction Derby',NULL),(378,'Soviet Strike',NULL),(379,'Tonic Trouble',NULL),(380,'Flight Simulator 2000',NULL); -/*!40000 ALTER TABLE `games` ENABLE KEYS */; -UNLOCK TABLES; +DROP TABLE IF EXISTS `notes`; +CREATE TABLE `notes` ( + `id` smallint unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + `content` text, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --- --- Table structure for table `platforms` --- +INSERT INTO `notes` (`id`, `title`, `content`) VALUES +(1, 'Note 1', 'Some comment 1.'), +(2, 'Note 2', 'Some comment 2'); DROP TABLE IF EXISTS `platforms`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `platforms` ( `id` tinyint unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`), UNIQUE KEY `platforms_name` (`name`) -) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `platforms` --- +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -LOCK TABLES `platforms` WRITE; -/*!40000 ALTER TABLE `platforms` DISABLE KEYS */; -INSERT INTO `platforms` VALUES (9,'Dreamcast'),(10,'Game Boy'),(5,'GameCube'),(7,'Megadrive II'),(4,'Nintendo 64'),(1,'PC'),(2,'Playstation'),(3,'Playstation 2'),(8,'Saturn'),(11,'Super Nintendo'),(6,'Wii'); -/*!40000 ALTER TABLE `platforms` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `stories` --- +INSERT INTO `platforms` (`id`, `name`) VALUES +(9, 'Dreamcast'), +(10, 'Game Boy'), +(5, 'GameCube'), +(7, 'Megadrive II'), +(4, 'Nintendo 64'), +(1, 'PC'), +(2, 'Playstation'), +(3, 'Playstation 2'), +(8, 'Saturn'), +(11, 'Super Nintendo'), +(6, 'Wii'); DROP TABLE IF EXISTS `stories`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `stories` ( `id` int unsigned NOT NULL AUTO_INCREMENT, - `version_id` smallint unsigned DEFAULT NULL, + `version_id` int unsigned DEFAULT NULL, `year` smallint unsigned NOT NULL, `position` smallint unsigned NOT NULL, `watched` tinyint unsigned NOT NULL DEFAULT '0', `played` tinyint unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `game_id` (`version_id`), - CONSTRAINT `stories_ibfk_1` FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) -) ENGINE=InnoDB AUTO_INCREMENT=91 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `stories` --- - -LOCK TABLES `stories` WRITE; -/*!40000 ALTER TABLE `stories` DISABLE KEYS */; -INSERT INTO `stories` VALUES (2,231,2018,1,1,0),(3,95,2018,2,1,0),(4,40,2019,1,1,0),(5,248,2019,3,1,0),(6,41,2019,4,1,0),(7,238,2019,5,1,0),(8,68,2019,6,1,0),(11,247,2020,2,1,0),(13,114,2020,5,1,0),(14,207,2020,8,1,0),(15,149,2020,9,1,0),(16,261,2020,7,1,0),(17,210,2020,1,1,0),(18,132,2020,10,1,0),(19,245,2020,11,1,0),(20,55,2020,12,1,0),(21,20,2021,1,1,0),(22,225,2021,2,1,0),(23,265,2021,3,1,0),(25,21,2021,4,1,0),(27,257,2020,3,1,0),(29,211,2020,13,1,0),(30,262,2020,14,1,0),(31,263,2020,15,1,0),(32,235,2020,16,1,0),(33,260,2021,5,1,0),(35,214,2020,4,1,0),(36,289,2020,18,0,1),(37,24,2017,1,1,0),(38,266,2021,6,1,0),(39,230,2021,7,1,0),(40,98,2021,9,0,1),(41,190,2019,7,1,0),(43,264,2021,8,1,0),(44,227,2021,10,1,0),(45,252,2021,11,1,0),(46,168,2021,12,1,0),(47,195,2021,13,1,0),(48,108,2020,6,1,0),(49,22,2021,14,1,0),(50,37,2021,15,1,0),(51,283,2021,16,1,0),(52,177,2021,17,0,1),(53,164,2021,18,1,0),(54,282,2021,19,0,1),(55,122,2021,20,1,0),(56,309,2021,21,1,0),(57,267,2021,22,1,0),(58,131,2021,24,0,1),(59,292,2019,2,0,1),(60,186,2020,17,0,1),(61,268,2021,25,1,0),(62,310,2021,23,0,1),(63,8,2021,26,1,1),(64,314,2021,27,1,0),(66,325,2021,28,0,1),(67,107,2021,29,0,1),(68,236,2021,30,1,0),(69,243,2019,1,1,0),(70,315,2021,32,1,0),(71,245,2021,33,1,0),(72,204,2020,19,0,1),(73,204,2021,34,0,1),(74,324,2022,1,1,0),(75,244,2018,3,0,1),(76,321,2022,2,1,0),(77,106,2022,3,0,1),(78,204,2022,4,1,1),(79,269,2022,5,1,0),(80,280,2022,6,1,0),(82,191,2022,7,1,0),(83,331,2022,8,1,0),(84,87,2021,35,0,1),(85,87,2022,9,1,1),(86,90,2022,10,0,1),(87,241,2022,11,1,0),(88,338,2022,12,1,0),(89,80,2022,13,0,1),(90,224,2022,14,0,1); -/*!40000 ALTER TABLE `stories` ENABLE KEYS */; -UNLOCK TABLES; + CONSTRAINT `stories_ibfk_1` FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --- --- Table structure for table `trades` --- +INSERT INTO `stories` (`id`, `version_id`, `year`, `position`, `watched`, `played`) VALUES +(2, 231, 2018, 1, 1, 0), +(3, 95, 2018, 2, 1, 0), +(4, 40, 2019, 1, 1, 0), +(5, 248, 2019, 3, 1, 0), +(6, 41, 2019, 4, 1, 0), +(7, 238, 2019, 5, 1, 0), +(8, 68, 2019, 6, 1, 0), +(11, 247, 2020, 2, 1, 0), +(13, 114, 2020, 5, 1, 0), +(14, 207, 2020, 8, 1, 0), +(15, 149, 2020, 9, 1, 0), +(16, 261, 2020, 7, 1, 0), +(17, 210, 2020, 1, 1, 0), +(18, 132, 2020, 10, 1, 0), +(19, 245, 2020, 11, 1, 0), +(20, 55, 2020, 12, 1, 0), +(21, 20, 2021, 1, 1, 0), +(22, 225, 2021, 2, 1, 0), +(23, 265, 2021, 3, 1, 0), +(25, 21, 2021, 4, 1, 0), +(27, 257, 2020, 3, 1, 0), +(29, 211, 2020, 13, 1, 0), +(30, 262, 2020, 14, 1, 0), +(31, 263, 2020, 15, 1, 0), +(32, 235, 2020, 16, 1, 0), +(33, 260, 2021, 5, 1, 0), +(35, 214, 2020, 4, 1, 0), +(36, 289, 2020, 18, 0, 1), +(37, 24, 2017, 1, 1, 0), +(38, 266, 2021, 6, 1, 0), +(39, 230, 2021, 7, 1, 0), +(40, 98, 2021, 9, 0, 1), +(41, 190, 2019, 7, 1, 0), +(43, 264, 2021, 8, 1, 0), +(44, 227, 2021, 10, 1, 0), +(45, 252, 2021, 11, 1, 0), +(46, 168, 2021, 12, 1, 0), +(47, 195, 2021, 13, 1, 0), +(48, 108, 2020, 6, 1, 0), +(49, 22, 2021, 14, 1, 0), +(50, 37, 2021, 15, 1, 0), +(51, 283, 2021, 16, 1, 0), +(52, 177, 2021, 17, 0, 1), +(53, 164, 2021, 18, 1, 0), +(54, 282, 2021, 19, 0, 1), +(55, 122, 2021, 20, 1, 0), +(56, 309, 2021, 21, 1, 0), +(57, 267, 2021, 22, 1, 0), +(58, 131, 2021, 24, 0, 1), +(59, 292, 2019, 2, 0, 1), +(60, 186, 2020, 17, 0, 1), +(61, 268, 2021, 25, 1, 0), +(62, 310, 2021, 23, 0, 1), +(63, 8, 2021, 26, 1, 1), +(64, 314, 2021, 27, 1, 0), +(66, 325, 2021, 28, 0, 1), +(67, 107, 2021, 29, 0, 1), +(68, 236, 2021, 30, 1, 0), +(69, 243, 2019, 1, 1, 0), +(70, 315, 2021, 32, 1, 0), +(71, 245, 2021, 33, 1, 0), +(72, 204, 2020, 19, 0, 1), +(73, 204, 2021, 34, 0, 1), +(74, 324, 2022, 1, 1, 0), +(75, 244, 2018, 3, 0, 1), +(76, 321, 2022, 2, 1, 0), +(77, 106, 2022, 3, 0, 1), +(78, 204, 2022, 4, 1, 1), +(79, 269, 2022, 5, 1, 0), +(80, 280, 2022, 6, 1, 0), +(82, 191, 2022, 7, 1, 0), +(83, 331, 2022, 8, 1, 0), +(84, 87, 2021, 35, 0, 1), +(85, 87, 2022, 9, 1, 1), +(86, 90, 2022, 10, 0, 1), +(87, 241, 2022, 11, 1, 0), +(88, 338, 2022, 12, 1, 0), +(89, 80, 2022, 13, 0, 1), +(90, 224, 2022, 14, 0, 1); DROP TABLE IF EXISTS `trades`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `trades` ( `trade_id` int unsigned NOT NULL AUTO_INCREMENT, - `copy_id` smallint unsigned NOT NULL, + `copy_id` int unsigned NOT NULL, `year` smallint unsigned NOT NULL, `month` smallint unsigned NOT NULL, `day` smallint unsigned NOT NULL, @@ -172,31 +589,18 @@ CREATE TABLE `trades` ( `notes` text, PRIMARY KEY (`trade_id`), KEY `copy_id` (`copy_id`), - CONSTRAINT `trades_ibfk_1` FOREIGN KEY (`copy_id`) REFERENCES `copies` (`copy_id`) -) ENGINE=InnoDB AUTO_INCREMENT=92 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `trades` --- - -LOCK TABLES `trades` WRITE; -/*!40000 ALTER TABLE `trades` DISABLE KEYS */; -INSERT INTO `trades` VALUES (90,1,2022,2,4,'Loan-out',''),(91,1,2022,4,8,'Loan-out-return',''); -/*!40000 ALTER TABLE `trades` ENABLE KEYS */; -UNLOCK TABLES; + CONSTRAINT `trades_ibfk_1` FOREIGN KEY (`copy_id`) REFERENCES `copies` (`copy_id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --- --- Table structure for table `transactions` --- +INSERT INTO `trades` (`trade_id`, `copy_id`, `year`, `month`, `day`, `type`, `notes`) VALUES +(90, 1, 2022, 2, 4, 'Loan-out', ''), +(91, 1, 2022, 4, 8, 'Loan-out-return', ''); DROP TABLE IF EXISTS `transactions`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `transactions` ( `transaction_id` int unsigned NOT NULL AUTO_INCREMENT, - `version_id` smallint unsigned NOT NULL, - `copy_id` smallint unsigned DEFAULT NULL, + `version_id` int unsigned NOT NULL, + `copy_id` int unsigned DEFAULT NULL, `year` smallint unsigned NOT NULL, `month` smallint unsigned NOT NULL, `day` smallint unsigned NOT NULL, @@ -205,28 +609,18 @@ CREATE TABLE `transactions` ( PRIMARY KEY (`transaction_id`), KEY `copy_id` (`copy_id`), KEY `transactions_ibfk_2` (`version_id`), - CONSTRAINT `transactions_ibfk_1` FOREIGN KEY (`copy_id`) REFERENCES `copies` (`copy_id`), - CONSTRAINT `transactions_ibfk_2` FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) -) ENGINE=InnoDB AUTO_INCREMENT=107 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; + CONSTRAINT `transactions_ibfk_1` FOREIGN KEY (`copy_id`) REFERENCES `copies` (`copy_id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `transactions_ibfk_2` FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; --- --- Dumping data for table `transactions` --- - -LOCK TABLES `transactions` WRITE; -/*!40000 ALTER TABLE `transactions` DISABLE KEYS */; -INSERT INTO `transactions` VALUES (90,348,1,2022,2,4,'Loan-out',''),(91,348,1,2022,4,8,'Loan-out-return',''),(92,349,2,2022,2,4,'Loan-out',''),(93,349,2,2022,2,5,'Loan-in',''),(106,340,NULL,2022,3,4,'Sold',NULL); -/*!40000 ALTER TABLE `transactions` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `users` --- +INSERT INTO `transactions` (`transaction_id`, `version_id`, `copy_id`, `year`, `month`, `day`, `type`, `notes`) VALUES +(90, 348, 1, 2022, 2, 4, 'Loan-out', ''), +(91, 348, 1, 2022, 4, 8, 'Loan-out-return', ''), +(92, 349, 2, 2022, 2, 4, 'Loan-out', ''), +(93, 349, 2, 2022, 2, 5, 'Loan-in', ''), +(106, 340, NULL, 2022, 3, 4, 'Sold', NULL); DROP TABLE IF EXISTS `users`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `users` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '', @@ -239,30 +633,16 @@ CREATE TABLE `users` ( UNIQUE KEY `email_2` (`email`), UNIQUE KEY `username` (`user_name`), KEY `email` (`email`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `users` --- +) ENGINE=InnoDB DEFAULT CHARSET=latin1; -LOCK TABLES `users` WRITE; -/*!40000 ALTER TABLE `users` DISABLE KEYS */; -INSERT INTO `users` VALUES (1,'foo@bar.com','802cee6fdb8f3964700a7d789bd034cee6e5dba100f2c3df1fcb2b9afdc97b2b','kK0pXVUq',1,'Eric','tokentest123'); -/*!40000 ALTER TABLE `users` ENABLE KEYS */; -UNLOCK TABLES; - --- --- Table structure for table `versions` --- +INSERT INTO `users` (`id`, `email`, `password`, `salt`, `status`, `user_name`, `token`) VALUES +(1, 'foo@bar.com', '802cee6fdb8f3964700a7d789bd034cee6e5dba100f2c3df1fcb2b9afdc97b2b', 'kK0pXVUq', 1, 'Eric', 'tokentest123'); DROP TABLE IF EXISTS `versions`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `versions` ( - `version_id` smallint unsigned NOT NULL AUTO_INCREMENT, + `version_id` int unsigned NOT NULL AUTO_INCREMENT, `platform_id` tinyint unsigned NOT NULL, - `game_id` smallint unsigned NOT NULL, + `game_id` int unsigned NOT NULL, `release_year` smallint NOT NULL DEFAULT '0', `todo_solo_sometimes` tinyint unsigned NOT NULL DEFAULT '0', `todo_multiplayer_sometimes` tinyint unsigned NOT NULL DEFAULT '0', @@ -289,25 +669,358 @@ CREATE TABLE `versions` ( UNIQUE KEY `games_platforms` (`platform_id`,`game_id`), KEY `game_id` (`game_id`), CONSTRAINT `versions_ibfk_1` FOREIGN KEY (`platform_id`) REFERENCES `platforms` (`id`), - CONSTRAINT `versions_ibfk_2` FOREIGN KEY (`game_id`) REFERENCES `games` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=350 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `versions` --- + CONSTRAINT `versions_ibfk_2` FOREIGN KEY (`game_id`) REFERENCES `games` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -LOCK TABLES `versions` WRITE; -/*!40000 ALTER TABLE `versions` DISABLE KEYS */; -INSERT INTO `versions` VALUES (1,7,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,'top en coop !!!',0,0,0,0,0),(2,7,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(3,7,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(4,7,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(5,7,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(6,7,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(7,7,7,0,0,0,0,0,0,0,0,0,0,0,1,1996,1,1,0,NULL,0,0,0,0,0),(8,8,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(9,8,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(10,8,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(11,8,11,0,1,0,0,0,0,0,0,0,0,0,1,1998,8,1,0,NULL,0,0,0,0,0),(12,8,13,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(13,8,14,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(14,8,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(15,8,16,0,0,0,0,0,0,0,0,0,0,0,1,1998,7,1,0,NULL,0,0,0,0,0),(16,2,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(17,2,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(18,2,19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(19,2,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(20,2,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(21,2,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(22,2,23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(23,2,24,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,8,0),(24,2,25,0,0,0,0,0,0,0,0,0,1,1,1,1999,4,0,0,NULL,0,1,0,0,0),(25,2,26,0,0,0,0,0,0,0,0,0,1,1,1,2001,3,0,0,NULL,0,0,0,0,0),(26,2,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(27,2,28,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(28,2,29,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(29,2,30,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(30,2,31,0,0,0,0,0,0,0,0,0,0,0,1,2000,3,0,0,NULL,0,0,0,0,0),(31,2,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(32,2,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(33,2,34,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(34,2,35,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(35,2,36,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(36,2,37,0,0,0,0,0,0,0,0,0,1,0,1,2000,5,0,0,NULL,0,0,0,0,0),(37,2,38,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(38,2,39,0,0,0,0,0,0,0,0,0,0,0,1,2000,6,1,0,NULL,0,0,0,0,0),(39,4,40,0,0,1,0,0,0,0,0,0,0,0,1,1997,2,1,0,NULL,0,0,0,0,0),(40,4,41,0,0,0,0,0,0,0,0,0,0,1,1,1999,2,0,0,NULL,0,1,0,0,0),(41,4,42,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(42,4,43,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(43,4,44,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(44,4,45,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(45,4,46,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(46,4,47,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(47,4,48,0,0,1,0,0,0,0,0,0,0,1,1,1998,6,1,0,NULL,0,1,0,0,0),(48,4,49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(49,9,50,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(50,9,51,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(51,9,52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(52,9,53,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(53,9,54,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(54,9,56,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(55,9,58,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(56,9,59,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(57,9,60,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(58,3,61,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(59,3,62,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(60,3,64,0,0,1,0,0,0,0,0,0,0,0,1,2002,4,1,0,NULL,0,0,0,0,0),(61,3,68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(62,3,69,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(63,3,70,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(64,3,71,0,0,0,0,0,0,0,0,0,1,1,1,2001,4,0,0,NULL,0,1,0,0,0),(65,3,72,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(66,3,73,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(67,5,74,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(68,5,75,0,0,0,0,0,0,0,0,0,0,0,1,2004,5,0,0,NULL,0,0,0,0,0),(69,5,76,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(70,5,21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(71,5,78,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(72,5,79,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,3,0),(73,5,80,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(74,5,81,0,0,1,0,0,0,0,0,0,0,0,1,2003,3,1,0,NULL,0,0,0,0,0),(75,5,82,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(76,5,83,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(77,6,84,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(78,6,85,0,0,1,0,0,0,0,0,0,0,0,1,2008,1,1,0,NULL,0,0,0,0,0),(79,6,86,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(80,6,87,0,1,1,0,0,0,0,0,0,0,1,1,2022,5,0,0,NULL,0,0,0,0,0),(81,6,88,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(82,6,89,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(83,6,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(84,6,91,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(85,6,92,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(86,6,93,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(87,6,94,0,1,1,0,0,0,0,0,0,0,0,1,2021,5,0,0,NULL,0,0,0,0,0),(88,6,95,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(89,1,96,0,0,0,0,0,1,0,0,0,0,1,1,1998,1,1,0,NULL,0,1,0,10,0),(90,1,97,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(91,1,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(92,1,99,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(93,1,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(94,1,101,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(95,1,102,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(96,1,104,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(97,1,105,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(98,1,106,0,0,0,1,0,0,0,0,0,0,0,1,2007,2,0,0,NULL,1,0,0,0,0),(99,1,107,0,0,0,0,0,0,0,0,0,0,1,1,1999,6,1,0,NULL,0,0,0,0,0),(100,1,108,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(101,1,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(102,1,110,0,0,0,0,1,1,0,0,0,0,1,1,2003,2,1,0,NULL,0,1,0,0,0),(103,1,111,0,0,0,1,1,0,0,0,0,0,1,1,2006,1,0,0,NULL,0,0,0,0,0),(104,1,112,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(105,1,113,0,0,0,1,1,0,0,0,0,0,1,1,2004,1,1,0,NULL,0,0,0,0,0),(106,1,114,0,0,0,0,1,0,0,0,0,0,0,1,1999,8,0,1,NULL,0,0,0,0,0),(107,1,115,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(108,1,116,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(109,1,117,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(110,1,38,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(111,1,119,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(112,1,120,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(113,1,121,0,0,0,0,0,0,0,0,0,0,0,1,1999,3,1,0,NULL,0,0,0,0,0),(114,1,122,0,0,0,0,0,1,0,0,0,0,0,1,1998,10,0,0,NULL,0,0,0,6,0),(115,1,123,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(116,1,124,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(117,1,125,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(118,1,126,0,0,0,1,0,0,0,0,0,0,1,1,2001,2,1,0,NULL,0,0,0,0,0),(119,1,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(120,1,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(121,1,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(122,1,130,0,0,0,0,0,0,0,0,0,1,1,1,1997,1,0,0,NULL,0,1,0,0,0),(123,1,132,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(124,1,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(125,1,134,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(126,1,135,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(127,1,136,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(128,1,137,0,0,0,0,0,0,0,0,0,0,0,1,1999,1,1,0,NULL,0,1,0,0,0),(129,1,138,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(130,1,139,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(131,1,140,0,0,0,0,0,0,0,0,0,0,1,1,2001,1,1,0,NULL,0,1,0,0,0),(132,1,141,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(133,1,142,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(134,1,143,0,0,0,0,1,0,0,0,0,0,1,1,2001,8,1,0,NULL,0,0,0,0,0),(135,1,144,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(136,1,145,0,0,0,0,0,0,0,0,0,0,1,1,2001,6,0,0,NULL,0,0,0,0,0),(137,1,146,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(138,1,147,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(139,1,148,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(140,1,149,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(141,1,150,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(142,1,152,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(143,1,153,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(144,1,154,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,1,0,0,0,0),(145,1,155,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(146,1,156,0,0,0,0,0,0,0,0,0,0,0,1,2001,5,0,0,NULL,0,0,0,0,0),(147,1,157,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(148,1,158,0,0,0,1,0,0,0,0,0,0,1,1,1998,3,1,0,NULL,0,1,0,0,0),(149,1,159,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(150,1,160,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(151,1,161,0,0,0,0,0,0,1,0,0,0,1,1,2005,3,0,0,NULL,0,0,0,0,0),(152,1,162,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(153,1,163,0,0,0,1,1,0,1,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(154,1,164,0,0,0,0,0,0,0,0,0,0,0,1,2004,4,0,0,NULL,0,0,0,0,0),(155,1,165,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(156,1,166,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(157,1,167,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(158,1,168,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(159,1,169,0,1,0,0,0,0,0,0,0,0,0,1,2002,6,1,0,NULL,0,0,0,0,0),(160,1,170,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(161,1,171,0,0,0,0,0,0,0,0,0,0,0,1,1998,2,1,0,NULL,0,0,0,0,0),(162,1,172,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(163,1,174,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,NULL,1,0,0,0,0),(164,1,175,0,0,0,0,0,0,0,0,0,0,1,1,2000,1,0,0,NULL,1,0,0,0,0),(165,1,176,0,0,0,0,0,0,0,0,0,0,0,1,1996,2,0,0,NULL,0,0,0,0,0),(166,1,177,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(167,1,178,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(168,1,179,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(169,1,180,0,0,0,0,0,1,0,0,0,0,0,1,2001,7,0,0,NULL,1,0,0,13,0),(170,1,181,0,0,0,1,0,0,0,0,0,0,0,1,2002,1,0,0,NULL,0,1,0,0,0),(171,1,182,0,0,0,0,0,0,0,0,0,0,0,1,2010,1,0,0,NULL,0,0,0,0,0),(172,1,183,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(173,1,184,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(174,1,185,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(175,1,186,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(176,1,187,0,0,0,0,1,0,0,0,0,0,0,1,2002,3,1,0,NULL,0,0,0,0,0),(177,1,188,0,0,0,0,0,0,0,0,0,0,0,1,1999,7,0,0,NULL,1,0,0,0,0),(178,1,189,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(179,1,190,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(180,1,191,0,0,0,1,1,0,0,0,0,0,1,1,2005,1,1,0,NULL,0,1,0,0,0),(181,1,192,0,0,0,0,0,0,0,0,0,0,0,1,2005,2,1,0,NULL,0,0,0,0,0),(182,1,193,0,0,0,0,0,0,0,0,0,0,1,1,2004,2,0,0,NULL,0,0,0,0,0),(183,1,194,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(184,1,195,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(185,1,196,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(186,1,197,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(187,1,198,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(188,1,199,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(189,1,200,0,0,0,0,0,0,0,0,0,0,1,1,1998,5,1,0,NULL,0,0,0,0,0),(190,1,201,0,0,0,0,0,0,0,0,0,0,0,1,2012,1,0,0,NULL,0,0,0,0,0),(191,1,202,0,0,0,0,0,0,0,0,0,0,0,1,2022,3,0,0,NULL,0,0,0,0,0),(192,1,203,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(193,1,204,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(194,1,205,0,0,0,1,0,0,0,0,0,0,1,1,1998,4,1,0,NULL,0,0,0,0,0),(195,1,206,0,0,0,0,0,0,0,0,0,0,0,1,1998,8,0,0,NULL,1,0,0,0,0),(196,1,207,0,1,0,0,0,0,0,0,0,0,1,1,2003,1,1,0,NULL,0,0,0,0,0),(197,1,208,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1,0,NULL,0,0,0,0,0),(198,1,209,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(199,1,210,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(200,1,211,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(201,1,213,0,0,0,0,0,0,0,0,0,0,0,1,2022,1,0,0,NULL,0,1,0,0,0),(202,1,214,0,0,0,0,0,1,0,0,0,0,0,1,2007,1,0,0,NULL,0,0,0,0,0),(203,1,215,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(204,1,217,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(205,1,218,0,0,0,0,0,0,0,0,0,0,0,1,2009,1,0,0,NULL,0,0,0,0,0),(206,1,219,0,1,1,0,0,0,0,0,0,0,0,1,2000,4,1,0,NULL,0,0,0,0,0),(207,1,220,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,99,0),(208,1,222,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1,0,NULL,0,0,0,0,0),(209,1,223,0,0,0,1,0,0,0,0,0,0,1,1,1997,3,1,0,NULL,0,0,0,0,0),(210,4,224,0,0,0,0,0,0,0,0,0,0,1,1,2020,2,0,0,NULL,0,0,0,0,0),(211,1,225,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,12,0),(212,1,229,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(213,1,230,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(214,1,231,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(215,1,232,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(216,1,233,0,0,0,0,0,0,0,0,0,0,0,0,2005,4,0,0,NULL,0,0,0,0,0),(217,1,234,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(218,1,236,0,0,0,0,0,0,0,0,0,0,0,1,2000,2,1,0,NULL,0,0,0,0,0),(219,1,237,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(220,1,16,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(221,1,239,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(222,1,240,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(223,1,241,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,1,0,0,0,0),(224,1,243,0,0,0,0,0,0,0,0,0,0,0,1,2022,2,0,0,NULL,0,0,0,0,0),(225,1,245,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(226,1,246,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(227,1,247,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(228,1,248,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(229,1,249,0,0,0,0,0,0,0,0,0,0,0,1,2004,3,0,0,NULL,0,0,0,0,0),(230,1,250,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(231,1,251,0,0,0,0,0,0,0,0,0,1,0,1,2018,1,0,0,NULL,0,1,0,0,0),(232,10,252,0,0,0,0,0,0,1,0,0,0,1,1,1993,1,1,0,NULL,0,0,0,0,0),(233,1,253,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(234,1,255,0,0,0,0,0,1,0,0,0,0,0,1,2002,5,0,0,NULL,0,0,0,9,0),(235,1,256,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(236,1,257,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(237,1,258,0,0,0,1,0,0,0,0,0,0,1,1,2002,5,1,0,NULL,0,0,0,0,0),(238,2,259,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(239,1,260,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(240,1,261,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(241,1,262,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(242,1,263,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(243,1,264,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(244,1,265,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(245,1,267,0,0,0,0,0,0,0,0,0,1,0,1,2020,3,0,0,NULL,0,0,0,0,0),(246,1,269,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(247,1,270,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(248,2,271,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(249,1,273,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(250,1,274,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(251,1,275,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(252,1,276,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(253,1,277,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(254,1,279,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(255,1,281,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(256,1,282,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(257,1,283,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(258,1,284,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(259,1,285,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,1,0,0,0,0),(260,1,286,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(261,2,287,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(262,1,288,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(263,1,289,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(264,1,290,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(265,1,291,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(266,1,292,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(267,1,293,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(268,1,294,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(269,1,295,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(270,1,296,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(271,1,297,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(272,1,298,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(273,1,299,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(274,3,300,0,0,0,0,0,0,0,0,0,0,0,1,2002,2,0,0,NULL,0,0,0,0,0),(275,4,301,0,0,0,0,0,0,0,0,0,0,0,1,1999,5,0,0,NULL,0,0,0,0,0),(276,1,29,0,0,0,0,0,0,0,0,0,0,0,1,1997,4,1,0,NULL,0,0,0,0,0),(277,4,27,0,0,0,0,0,0,0,0,0,0,0,1,1997,4,0,0,NULL,0,0,0,0,0),(278,6,304,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(279,1,305,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(280,1,307,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(281,9,308,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,8,0),(282,1,309,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(283,1,310,0,0,0,0,0,0,0,0,0,0,0,1,2021,1,0,0,NULL,0,0,0,0,0),(284,1,311,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(285,1,312,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(286,1,313,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(287,1,314,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(288,1,315,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(289,1,316,0,0,0,1,1,0,0,0,0,0,1,1,2020,1,1,0,NULL,0,0,0,0,0),(290,1,319,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(291,1,320,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(292,1,321,0,0,0,1,0,0,0,0,0,0,1,1,2019,1,1,0,NULL,0,0,0,0,0),(293,1,322,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(294,1,323,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,1,0,0,0,0),(295,1,325,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(296,1,326,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(297,1,327,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(298,1,328,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(299,1,329,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(300,1,330,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(301,1,331,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(302,1,332,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(303,3,333,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(304,1,334,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(305,1,335,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(306,1,287,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(307,1,337,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(308,1,338,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(309,1,339,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(310,1,340,0,1,0,0,0,0,0,0,0,0,1,1,2006,2,0,0,NULL,0,0,0,0,0),(311,4,341,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(312,4,342,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(313,4,343,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(314,1,344,0,0,0,0,0,0,0,0,0,0,0,1,2021,3,0,0,NULL,0,0,0,0,0),(315,2,345,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(316,11,346,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(317,10,347,0,0,0,0,0,0,0,0,0,0,0,1,1996,3,1,0,NULL,0,0,0,0,0),(318,1,348,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,NULL,0,0,0,0,0),(319,1,349,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(320,5,350,0,0,0,0,0,0,0,0,0,0,0,1,2021,2,0,0,NULL,0,0,0,0,0),(321,1,351,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(322,1,352,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(323,2,240,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,7,0),(324,2,354,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(325,4,355,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(326,4,356,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(327,11,357,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(328,11,358,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,11,0),(329,4,359,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(330,1,360,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(331,1,361,0,0,0,0,0,0,0,0,0,0,0,1,2022,4,0,0,NULL,0,0,0,0,0),(332,1,362,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(333,1,363,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(334,3,364,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(335,1,366,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(336,1,367,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(337,1,368,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,1,0,0,0,0),(338,1,369,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(339,1,370,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,1,0,0,0,0),(340,1,371,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(341,1,372,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,1,0,0,0,0),(342,1,373,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,1,0,0,0,0),(343,1,374,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(344,2,375,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(345,1,376,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(346,8,377,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,4,0),(347,8,378,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,1,0,0,5,0),(348,1,379,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0),(349,1,380,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,NULL,0,0,0,0,0); -/*!40000 ALTER TABLE `versions` ENABLE KEYS */; -UNLOCK TABLES; -/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; +INSERT INTO `versions` (`version_id`, `platform_id`, `game_id`, `release_year`, `todo_solo_sometimes`, `todo_multiplayer_sometimes`, `singleplayer_recurring`, `multiplayer_recurring`, `to_do`, `to_buy`, `to_watch_background`, `to_watch_serious`, `to_rewatch`, `top_game`, `hall_of_fame`, `hall_of_fame_year`, `hall_of_fame_position`, `played_it_often`, `ongoing`, `comments`, `todo_with_help`, `bgf`, `to_watch_position`, `to_do_position`, `finished`) VALUES +(1, 7, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 'top en coop !!!', 0, 0, 0, 0, 0), +(2, 7, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(3, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(4, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(5, 7, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(6, 7, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1996, 1, 1, 0, NULL, 0, 0, 0, 0, 0), +(8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(9, 8, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(10, 8, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(11, 8, 11, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1998, 8, 1, 0, NULL, 0, 0, 0, 0, 0), +(12, 8, 13, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(13, 8, 14, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(14, 8, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(15, 8, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1998, 7, 1, 0, NULL, 0, 0, 0, 0, 0), +(16, 2, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(17, 2, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(18, 2, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(19, 2, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(20, 2, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(21, 2, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(22, 2, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(23, 2, 24, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 8, 0), +(24, 2, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1999, 4, 0, 0, NULL, 0, 1, 0, 0, 0), +(25, 2, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2001, 3, 0, 0, NULL, 0, 0, 0, 0, 0), +(26, 2, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(27, 2, 28, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(28, 2, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(29, 2, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(30, 2, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2000, 3, 0, 0, NULL, 0, 0, 0, 0, 0), +(31, 2, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(32, 2, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(33, 2, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(34, 2, 35, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(35, 2, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(36, 2, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 2000, 5, 0, 0, NULL, 0, 0, 0, 0, 0), +(37, 2, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(38, 2, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2000, 6, 1, 0, NULL, 0, 0, 0, 0, 0), +(39, 4, 40, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1997, 2, 1, 0, NULL, 0, 0, 0, 0, 0), +(40, 4, 41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1999, 2, 0, 0, NULL, 0, 1, 0, 0, 0), +(41, 4, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(42, 4, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(43, 4, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(44, 4, 45, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(45, 4, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(46, 4, 47, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(47, 4, 48, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1998, 6, 1, 0, NULL, 0, 1, 0, 0, 0), +(48, 4, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(49, 9, 50, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(50, 9, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(51, 9, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(52, 9, 53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(53, 9, 54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(54, 9, 56, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(55, 9, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(56, 9, 59, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(57, 9, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(58, 3, 61, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(59, 3, 62, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(60, 3, 64, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2002, 4, 1, 0, NULL, 0, 0, 0, 0, 0), +(61, 3, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(62, 3, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(63, 3, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(64, 3, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2001, 4, 0, 0, NULL, 0, 1, 0, 0, 0), +(65, 3, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(66, 3, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(67, 5, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(68, 5, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2004, 5, 0, 0, NULL, 0, 0, 0, 0, 0), +(69, 5, 76, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(70, 5, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(71, 5, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(72, 5, 79, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 3, 0), +(73, 5, 80, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(74, 5, 81, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2003, 3, 1, 0, NULL, 0, 0, 0, 0, 0), +(75, 5, 82, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(76, 5, 83, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(77, 6, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(78, 6, 85, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2008, 1, 1, 0, NULL, 0, 0, 0, 0, 0), +(79, 6, 86, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(80, 6, 87, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2022, 5, 0, 0, NULL, 0, 0, 0, 0, 0), +(81, 6, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(82, 6, 89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(83, 6, 90, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(84, 6, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(85, 6, 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(86, 6, 93, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(87, 6, 94, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2021, 5, 0, 0, NULL, 0, 0, 0, 0, 0), +(88, 6, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(89, 1, 96, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1998, 1, 1, 0, NULL, 0, 1, 0, 10, 0), +(90, 1, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(91, 1, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(92, 1, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(93, 1, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(94, 1, 101, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(95, 1, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(96, 1, 104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(97, 1, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(98, 1, 106, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 2007, 2, 0, 0, NULL, 1, 0, 0, 0, 0), +(99, 1, 107, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1999, 6, 1, 0, NULL, 0, 0, 0, 0, 0), +(100, 1, 108, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(101, 1, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(102, 1, 110, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 2003, 2, 1, 0, NULL, 0, 1, 0, 0, 0), +(103, 1, 111, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 2006, 1, 0, 0, NULL, 0, 0, 0, 0, 0), +(104, 1, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(105, 1, 113, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 2004, 1, 1, 0, NULL, 0, 0, 0, 0, 0), +(106, 1, 114, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1999, 8, 0, 1, NULL, 0, 0, 0, 0, 0), +(107, 1, 115, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(108, 1, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(109, 1, 117, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(110, 1, 38, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(111, 1, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(112, 1, 120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(113, 1, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1999, 3, 1, 0, NULL, 0, 0, 0, 0, 0), +(114, 1, 122, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1998, 10, 0, 0, NULL, 0, 0, 0, 6, 0), +(115, 1, 123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(116, 1, 124, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(117, 1, 125, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(118, 1, 126, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2001, 2, 1, 0, NULL, 0, 0, 0, 0, 0), +(119, 1, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(120, 1, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(121, 1, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(122, 1, 130, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1997, 1, 0, 0, NULL, 0, 1, 0, 0, 0), +(123, 1, 132, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(124, 1, 133, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(125, 1, 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(126, 1, 135, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(127, 1, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(128, 1, 137, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1999, 1, 1, 0, NULL, 0, 1, 0, 0, 0), +(129, 1, 138, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(130, 1, 139, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(131, 1, 140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2001, 1, 1, 0, NULL, 0, 1, 0, 0, 0), +(132, 1, 141, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(133, 1, 142, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(134, 1, 143, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 2001, 8, 1, 0, NULL, 0, 0, 0, 0, 0), +(135, 1, 144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(136, 1, 145, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2001, 6, 0, 0, NULL, 0, 0, 0, 0, 0), +(137, 1, 146, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(138, 1, 147, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(139, 1, 148, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(140, 1, 149, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(141, 1, 150, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(142, 1, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(143, 1, 153, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(144, 1, 154, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 1, 0, 0, 0, 0), +(145, 1, 155, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(146, 1, 156, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2001, 5, 0, 0, NULL, 0, 0, 0, 0, 0), +(147, 1, 157, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(148, 1, 158, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1998, 3, 1, 0, NULL, 0, 1, 0, 0, 0), +(149, 1, 159, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(150, 1, 160, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(151, 1, 161, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 2005, 3, 0, 0, NULL, 0, 0, 0, 0, 0), +(152, 1, 162, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(153, 1, 163, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(154, 1, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2004, 4, 0, 0, NULL, 0, 0, 0, 0, 0), +(155, 1, 165, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(156, 1, 166, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(157, 1, 167, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(158, 1, 168, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(159, 1, 169, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2002, 6, 1, 0, NULL, 0, 0, 0, 0, 0), +(160, 1, 170, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(161, 1, 171, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1998, 2, 1, 0, NULL, 0, 0, 0, 0, 0), +(162, 1, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(163, 1, 174, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, NULL, 1, 0, 0, 0, 0), +(164, 1, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2000, 1, 0, 0, NULL, 1, 0, 0, 0, 0), +(165, 1, 176, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1996, 2, 0, 0, NULL, 0, 0, 0, 0, 0), +(166, 1, 177, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(167, 1, 178, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(168, 1, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(169, 1, 180, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 2001, 7, 0, 0, NULL, 1, 0, 0, 13, 0), +(170, 1, 181, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 2002, 1, 0, 0, NULL, 0, 1, 0, 0, 0), +(171, 1, 182, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2010, 1, 0, 0, NULL, 0, 0, 0, 0, 0), +(172, 1, 183, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(173, 1, 184, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(174, 1, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(175, 1, 186, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(176, 1, 187, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 2002, 3, 1, 0, NULL, 0, 0, 0, 0, 0), +(177, 1, 188, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1999, 7, 0, 0, NULL, 1, 0, 0, 0, 0), +(178, 1, 189, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(179, 1, 190, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(180, 1, 191, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 2005, 1, 1, 0, NULL, 0, 1, 0, 0, 0), +(181, 1, 192, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2005, 2, 1, 0, NULL, 0, 0, 0, 0, 0), +(182, 1, 193, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2004, 2, 0, 0, NULL, 0, 0, 0, 0, 0), +(183, 1, 194, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(184, 1, 195, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(185, 1, 196, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(186, 1, 197, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(187, 1, 198, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(188, 1, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(189, 1, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1998, 5, 1, 0, NULL, 0, 0, 0, 0, 0), +(190, 1, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2012, 1, 0, 0, NULL, 0, 0, 0, 0, 0), +(191, 1, 202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2022, 3, 0, 0, NULL, 0, 0, 0, 0, 0), +(192, 1, 203, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(193, 1, 204, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(194, 1, 205, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1998, 4, 1, 0, NULL, 0, 0, 0, 0, 0), +(195, 1, 206, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1998, 8, 0, 0, NULL, 1, 0, 0, 0, 0), +(196, 1, 207, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2003, 1, 1, 0, NULL, 0, 0, 0, 0, 0), +(197, 1, 208, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(198, 1, 209, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(199, 1, 210, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(200, 1, 211, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(201, 1, 213, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2022, 1, 0, 0, NULL, 0, 1, 0, 0, 0), +(202, 1, 214, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 2007, 1, 0, 0, NULL, 0, 0, 0, 0, 0), +(203, 1, 215, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(204, 1, 217, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(205, 1, 218, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2009, 1, 0, 0, NULL, 0, 0, 0, 0, 0), +(206, 1, 219, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2000, 4, 1, 0, NULL, 0, 0, 0, 0, 0), +(207, 1, 220, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 99, 0), +(208, 1, 222, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(209, 1, 223, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1997, 3, 1, 0, NULL, 0, 0, 0, 0, 0), +(210, 4, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2020, 2, 0, 0, NULL, 0, 0, 0, 0, 0), +(211, 1, 225, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 12, 0), +(212, 1, 229, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(213, 1, 230, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(214, 1, 231, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(215, 1, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(216, 1, 233, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2005, 4, 0, 0, NULL, 0, 0, 0, 0, 0), +(217, 1, 234, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(218, 1, 236, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2000, 2, 1, 0, NULL, 0, 0, 0, 0, 0), +(219, 1, 237, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(220, 1, 16, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(221, 1, 239, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(222, 1, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(223, 1, 241, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 1, 0, 0, 0, 0), +(224, 1, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2022, 2, 0, 0, NULL, 0, 0, 0, 0, 0), +(225, 1, 245, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(226, 1, 246, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(227, 1, 247, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(228, 1, 248, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(229, 1, 249, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2004, 3, 0, 0, NULL, 0, 0, 0, 0, 0), +(230, 1, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(231, 1, 251, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 2018, 1, 0, 0, NULL, 0, 1, 0, 0, 0), +(232, 10, 252, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1993, 1, 1, 0, NULL, 0, 0, 0, 0, 0), +(233, 1, 253, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(234, 1, 255, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 2002, 5, 0, 0, NULL, 0, 0, 0, 9, 0), +(235, 1, 256, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(236, 1, 257, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(237, 1, 258, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2002, 5, 1, 0, NULL, 0, 0, 0, 0, 0), +(238, 2, 259, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(239, 1, 260, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(240, 1, 261, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(241, 1, 262, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(242, 1, 263, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(243, 1, 264, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(244, 1, 265, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(245, 1, 267, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 2020, 3, 0, 0, NULL, 0, 0, 0, 0, 0), +(246, 1, 269, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(247, 1, 270, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(248, 2, 271, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(249, 1, 273, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(250, 1, 274, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(251, 1, 275, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(252, 1, 276, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(253, 1, 277, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(254, 1, 279, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(255, 1, 281, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(256, 1, 282, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(257, 1, 283, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(258, 1, 284, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(259, 1, 285, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 1, 0, 0, 0, 0), +(260, 1, 286, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(261, 2, 287, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(262, 1, 288, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(263, 1, 289, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(264, 1, 290, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(265, 1, 291, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(266, 1, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(267, 1, 293, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(268, 1, 294, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(269, 1, 295, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(270, 1, 296, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(271, 1, 297, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(272, 1, 298, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(273, 1, 299, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(274, 3, 300, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2002, 2, 0, 0, NULL, 0, 0, 0, 0, 0), +(275, 4, 301, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1999, 5, 0, 0, NULL, 0, 0, 0, 0, 0), +(276, 1, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1997, 4, 1, 0, NULL, 0, 0, 0, 0, 0), +(277, 4, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1997, 4, 0, 0, NULL, 0, 0, 0, 0, 0), +(278, 6, 304, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(279, 1, 305, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(280, 1, 307, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(281, 9, 308, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 8, 0), +(282, 1, 309, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(283, 1, 310, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2021, 1, 0, 0, NULL, 0, 0, 0, 0, 0), +(284, 1, 311, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(285, 1, 312, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(286, 1, 313, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(287, 1, 314, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(288, 1, 315, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(289, 1, 316, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 2020, 1, 1, 0, NULL, 0, 0, 0, 0, 0), +(290, 1, 319, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(291, 1, 320, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(292, 1, 321, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 2019, 1, 1, 0, NULL, 0, 0, 0, 0, 0), +(293, 1, 322, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(294, 1, 323, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 1, 0, 0, 0, 0), +(295, 1, 325, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(296, 1, 326, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(297, 1, 327, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(298, 1, 328, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(299, 1, 329, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(300, 1, 330, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(301, 1, 331, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(302, 1, 332, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(303, 3, 333, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(304, 1, 334, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(305, 1, 335, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(306, 1, 287, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(307, 1, 337, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(308, 1, 338, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(309, 1, 339, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(310, 1, 340, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2006, 2, 0, 0, NULL, 0, 0, 0, 0, 0), +(311, 4, 341, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(312, 4, 342, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(313, 4, 343, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(314, 1, 344, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2021, 3, 0, 0, NULL, 0, 0, 0, 0, 0), +(315, 2, 345, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(316, 11, 346, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(317, 10, 347, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1996, 3, 1, 0, NULL, 0, 0, 0, 0, 0), +(318, 1, 348, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, NULL, 0, 0, 0, 0, 0), +(319, 1, 349, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(320, 5, 350, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2021, 2, 0, 0, NULL, 0, 0, 0, 0, 0), +(321, 1, 351, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(322, 1, 352, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(323, 2, 240, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 7, 0), +(324, 2, 354, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(325, 4, 355, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(326, 4, 356, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(327, 11, 357, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(328, 11, 358, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 11, 0), +(329, 4, 359, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(330, 1, 360, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(331, 1, 361, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2022, 4, 0, 0, NULL, 0, 0, 0, 0, 0), +(332, 1, 362, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(333, 1, 363, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(334, 3, 364, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(335, 1, 366, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(336, 1, 367, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(337, 1, 368, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 1, 0, 0, 0, 0), +(338, 1, 369, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(339, 1, 370, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 1, 0, 0, 0, 0), +(340, 1, 371, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(341, 1, 372, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 1, 0, 0, 0, 0), +(342, 1, 373, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 1, 0, 0, 0, 0), +(343, 1, 374, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(344, 2, 375, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(345, 1, 376, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(346, 8, 377, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 4, 0), +(347, 8, 378, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 1, 0, 0, 5, 0), +(348, 1, 379, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0), +(349, 1, 380, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, 0, 0); -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; +-- 2026-04-22 12:16:44 UTC From 4ee6edd29bbef56dc337ec4a3c975a9175a90995 Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Sat, 16 May 2026 15:24:34 +0200 Subject: [PATCH 08/18] Add a new fixture for a magazine issue and fix the patch for existing magazine --- src/service/magazine_service.py | 2 +- test/functional/test_magazine_issues.py | 16 ++++++++-------- test/functional/test_magazines.py | 4 ++-- test/games_test.sql | 7 ++++--- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/service/magazine_service.py b/src/service/magazine_service.py index 15604cc..9b6448f 100644 --- a/src/service/magazine_service.py +++ b/src/service/magazine_service.py @@ -35,7 +35,7 @@ def get_for_update(self, magazine_id: int) -> Magazine: existing = self.repository.get_by_title(title) if existing is not None and existing.get_id() != magazine.get_id(): - raise ResourceAlreadyExistsException('magazine', magazine.get_title(), 'title') + raise ResourceAlreadyExistsException('magazine', title, 'title') super().hydrate_for_update(magazine) diff --git a/test/functional/test_magazine_issues.py b/test/functional/test_magazine_issues.py index ed64c96..c9837ee 100644 --- a/test/functional/test_magazine_issues.py +++ b/test/functional/test_magazine_issues.py @@ -16,7 +16,7 @@ def test_get_magazine_issue(self): resp = self.api_call('get', 'magazine-issue/1', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual({'id': 1, 'magazineId': 1, 'issueNumber': 1, 'year': 1997, 'month': 8, 'notes': 'Le premier !'}, resp.json()) + self.assertEqual({'id': 1, 'magazineId': 1, 'issueNumber': 3, 'year': 1997, 'month': 10, 'notes': 'Le troisième !'}, resp.json()) def test_create_incomplete_payload(self): resp = self.api_call('post', 'magazine-issue', {}, True) @@ -38,15 +38,15 @@ def test_create_duplicate_issue_number(self): def test_create_update_delete_success(self): # Create - payload = {'magazineId': 1, 'issueNumber': 3, 'year': 1997, 'month': 10, 'notes': 'Le troisième.'} + payload = {'magazineId': 1, 'issueNumber': 4, 'year': 1998, 'month': 1, 'notes': 'Le quatrième.'} resp = self.api_call('post', 'magazine-issue', payload, True) self.assertEqual(200, resp.status_code) - self.assertEqual(3, resp.json()['issueNumber']) + self.assertEqual(4, resp.json()['issueNumber']) issue_id = str(resp.json()['id']) resp = self.api_call('get', 'magazine-issue/' + issue_id, None, True) - payload['id'] = 3 + payload['id'] = 4 self.assertEqual(payload, resp.json()) # Patch @@ -70,7 +70,7 @@ def test_update_not_found(self): self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) def test_update_duplicate_issue_number(self): - resp = self.api_call('patch', 'magazine-issue/2', {'issueNumber': 1}, True) + resp = self.api_call('patch', 'magazine-issue/3', {'issueNumber': 1}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue' with issue_number '1' already exists.", 'code': 8}, resp.json()) @@ -97,8 +97,8 @@ def test_get_list_default_filters(self): resp = self.api_call('get', 'magazine-issues', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual(2, resp.json()['resultCount']) - self.assertEqual(2, resp.json()['totalResultCount']) + self.assertEqual(3, resp.json()['resultCount']) + self.assertEqual(3, resp.json()['totalResultCount']) self.assertEqual(1, resp.json()['page']) self.assertEqual(1, resp.json()['totalPageCount']) @@ -106,7 +106,7 @@ def test_get_list_filter_by_magazine(self): resp = self.api_call('get', 'magazine-issues?magazineId[]=1', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual(2, resp.json()['resultCount']) + self.assertEqual(3, resp.json()['resultCount']) resp = self.api_call('get', 'magazine-issues?magazineId[]=2', {}, True) diff --git a/test/functional/test_magazines.py b/test/functional/test_magazines.py index e934ba3..a156224 100644 --- a/test/functional/test_magazines.py +++ b/test/functional/test_magazines.py @@ -16,7 +16,7 @@ def test_get_magazine(self): resp = self.api_call('get', 'magazine/1', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual({'id': 1, 'title': 'Gen4', 'notes': 'Découvert en 1997.', 'issueCount': 2}, resp.json()) + self.assertEqual({'id': 1, 'title': 'Gen4', 'notes': 'Découvert en 1997.', 'issueCount': 3}, resp.json()) def test_create_incomplete_payload(self): resp = self.api_call('post', 'magazine', {}, True) @@ -69,7 +69,7 @@ def test_update_duplicate_title(self): resp = self.api_call('patch', 'magazine/2', {'title': 'Gen4'}, True) self.assertEqual(400, resp.status_code) - self.assertEqual({'message': "The resource of type 'magazine' with title 'PC PLayer' already exists.", 'code': 8}, resp.json()) + self.assertEqual({'message': "The resource of type 'magazine' with title 'Gen4' already exists.", 'code': 8}, resp.json()) def test_delete_fails_because_magazine_has_issues(self): resp = self.api_call('delete', 'magazine/1', {}, True) diff --git a/test/games_test.sql b/test/games_test.sql index 30ad5d9..72d5568 100644 --- a/test/games_test.sql +++ b/test/games_test.sql @@ -435,8 +435,9 @@ CREATE TABLE `magazine_issues` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; INSERT INTO `magazine_issues` (`id`, `magazine_id`, `issue_number`, `year`, `month`, `notes`) VALUES -(1, 1, 1, 1997, 8, 'Le premier !'), -(2, 1, 2, 1997, 9, 'Le second.'); +(1, 1, 3, 1997, 10, 'Le troisième !'), +(2, 1, 1, 1997, 8, 'Le premier !'), +(3, 1, 2, 1997, 9, 'Le second.'); DROP TABLE IF EXISTS `magazines`; CREATE TABLE `magazines` ( @@ -448,7 +449,7 @@ CREATE TABLE `magazines` ( INSERT INTO `magazines` (`id`, `title`, `notes`) VALUES (1, 'Gen4', 'Découvert en 1997.'), -(2, 'PC PLayer', 'Découvert début 1998.'); +(2, 'PC Player', 'Découvert début 1998.'); DROP TABLE IF EXISTS `notes`; CREATE TABLE `notes` ( From 3be5e9640b5cd1f8545b9ec25fd69847132ee7ae Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Sun, 17 May 2026 16:05:23 +0200 Subject: [PATCH 09/18] Update tests --- docs/SETUP.md | 2 +- src/entity/game_version_magazine_mention.py | 14 +++++++++++++- src/entity/magazine_issue_copy.py | 2 +- .../test_game_version_magazine_mentions.py | 12 ++++++------ test/functional/test_magazine_issue_copies.py | 4 ++-- test/games_test.sql | 15 ++++++++------- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/docs/SETUP.md b/docs/SETUP.md index 280c860..5fb955a 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -6,7 +6,7 @@ In the root folder, copy the file _configuration.json.dist_ to _configuration.js Next, just do a __make start__, and then run __make test__ to run the tests and import the local DB with test features. You're good to go! -The test DB which was just imported has already a given user. You can login with the following credentials : +The test DB which was just imported has already a given user but you still need to run the test suite at least once. After that, you can login with the following credentials : * username: _mephistophelesz_ * password: _barz_ diff --git a/src/entity/game_version_magazine_mention.py b/src/entity/game_version_magazine_mention.py index d54b4f5..cf720f7 100644 --- a/src/entity/game_version_magazine_mention.py +++ b/src/entity/game_version_magazine_mention.py @@ -23,7 +23,19 @@ class GameVersionMagazineMention(AbstractEntity): 'method': '_type', 'required': True, 'type': 'strict-text', - 'allowed_values': {'Preview', 'Test', 'Guide', 'Other', 'Playable-demo', 'Watchable-demo'} + 'allowed_values': { + 'Advertisement', + 'Comparison', + 'Guide', + 'Mention', + 'Playable-demo', + 'Other', + 'Preview', + 'Short preview', + 'Short test', + 'Test', + 'Watchable-demo', + } }, 'notes': { 'field': 'notes', diff --git a/src/entity/magazine_issue_copy.py b/src/entity/magazine_issue_copy.py index ad1f9fc..99fafd3 100644 --- a/src/entity/magazine_issue_copy.py +++ b/src/entity/magazine_issue_copy.py @@ -18,7 +18,7 @@ class MagazineIssueCopy(AbstractEntity): 'method': '_type', 'required': True, 'type': 'strict-text', - 'allowed_values': {'Digital', 'Paper'} + 'allowed_values': {'Digital', 'Printed-Original', 'Printed-Copy'} }, 'notes': { 'field': 'notes', diff --git a/test/functional/test_game_version_magazine_mentions.py b/test/functional/test_game_version_magazine_mentions.py index 040dd28..f199d61 100644 --- a/test/functional/test_game_version_magazine_mentions.py +++ b/test/functional/test_game_version_magazine_mentions.py @@ -16,7 +16,7 @@ def test_get_mention(self): resp = self.api_call('get', 'game-version-magazine-mention/1', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual({'id': 1, 'magazineIssueId': 1, 'gameVersionId': 1, 'type': 'Test', 'notes': ''}, resp.json()) + self.assertEqual({'id': 1, 'magazineIssueId': 2, 'gameVersionId': 1, 'type': 'Test', 'notes': ''}, resp.json()) def test_create_incomplete_payload(self): resp = self.api_call('post', 'game-version-magazine-mention', {}, True) @@ -51,7 +51,7 @@ def test_create_update_delete_success(self): mention_id = str(resp.json()['id']) resp = self.api_call('get', 'game-version-magazine-mention/' + mention_id, None, True) - payload['id'] = 3 + payload['id'] = 4 self.assertEqual(payload, resp.json()) # Patch @@ -78,8 +78,8 @@ def test_get_list_default_filters(self): resp = self.api_call('get', 'game-version-magazine-mentions', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual(2, resp.json()['resultCount']) - self.assertEqual(2, resp.json()['totalResultCount']) + self.assertEqual(3, resp.json()['resultCount']) + self.assertEqual(3, resp.json()['totalResultCount']) self.assertEqual(1, resp.json()['page']) self.assertEqual(1, resp.json()['totalPageCount']) @@ -87,8 +87,8 @@ def test_get_list_filter_by_magazine_issue(self): resp = self.api_call('get', 'game-version-magazine-mentions?magazineIssueId[]=1', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual(1, resp.json()['resultCount']) - self.assertEqual(1, resp.json()['result'][0]['id']) + self.assertEqual(2, resp.json()['resultCount']) + self.assertEqual(2, resp.json()['result'][0]['id']) def test_get_list_filter_by_game_version(self): resp = self.api_call('get', 'game-version-magazine-mentions?gameVersionId[]=1', {}, True) diff --git a/test/functional/test_magazine_issue_copies.py b/test/functional/test_magazine_issue_copies.py index 9cb2565..87c728e 100644 --- a/test/functional/test_magazine_issue_copies.py +++ b/test/functional/test_magazine_issue_copies.py @@ -16,7 +16,7 @@ def test_get_magazine_issue_copy(self): resp = self.api_call('get', 'magazine-issue-copy/1', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual({'id': 1, 'magazineIssueId': 1, 'type': 'Paper', 'notes': 'Original'}, resp.json()) + self.assertEqual({'id': 1, 'magazineIssueId': 1, 'type': 'Printed-Original', 'notes': 'Original'}, resp.json()) def test_create_incomplete_payload(self): resp = self.api_call('post', 'magazine-issue-copy', {}, True) @@ -30,7 +30,7 @@ def test_create_unsupported_type(self): self.assertEqual(400, resp.status_code) def test_create_magazine_issue_not_found(self): - resp = self.api_call('post', 'magazine-issue-copy', {'magazineIssueId': 666, 'type': 'Paper'}, True) + resp = self.api_call('post', 'magazine-issue-copy', {'magazineIssueId': 666, 'type': 'Printed-Original'}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) diff --git a/test/games_test.sql b/test/games_test.sql index 72d5568..9b5a406 100644 --- a/test/games_test.sql +++ b/test/games_test.sql @@ -41,7 +41,7 @@ CREATE TABLE `game_version_magazine_mentions` ( `magazine_issue_id` int unsigned NOT NULL, `game_version_id` int unsigned NOT NULL, `type` varchar(255) NOT NULL, - `notes` text NOT NULL, + `notes` text NULL, PRIMARY KEY (`mention_id`), KEY `fk_mention_magazine_issue` (`magazine_issue_id`), KEY `fk_mention_game_version` (`game_version_id`), @@ -50,8 +50,9 @@ CREATE TABLE `game_version_magazine_mentions` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; INSERT INTO `game_version_magazine_mentions` (`mention_id`, `magazine_issue_id`, `game_version_id`, `type`, `notes`) VALUES -(1, 1, 1, 'Test', ''), -(2, 2, 1, 'Guide', ''); +(1, 2, 1, 'Test', ''), +(2, 1, 1, 'Guide', ''), +(3, 1, 36, 'Test', 'Super !'); DROP TABLE IF EXISTS `games`; CREATE TABLE `games` ( @@ -411,14 +412,14 @@ CREATE TABLE `magazine_issue_copies` ( `issue_copy_id` int unsigned NOT NULL AUTO_INCREMENT, `magazine_issue_id` int unsigned NOT NULL, `type` varchar(255) NOT NULL, - `notes` text NOT NULL, + `notes` text, PRIMARY KEY (`issue_copy_id`), KEY `fk_magazine_issue_copy_issue` (`magazine_issue_id`), CONSTRAINT `fk_magazine_issue_copy_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; INSERT INTO `magazine_issue_copies` (`issue_copy_id`, `magazine_issue_id`, `type`, `notes`) VALUES -(1, 1, 'Paper', 'Original'), +(1, 1, 'Printed-Original', 'Original'), (2, 1, 'Digital', 'Copy.'); DROP TABLE IF EXISTS `magazine_issues`; @@ -428,7 +429,7 @@ CREATE TABLE `magazine_issues` ( `issue_number` smallint unsigned NOT NULL, `year` smallint unsigned NOT NULL, `month` tinyint unsigned NOT NULL, - `notes` text NOT NULL, + `notes` text NULL, PRIMARY KEY (`id`), KEY `fk_magazine_issue_magazine` (`magazine_id`), CONSTRAINT `fk_magazine_issue_magazine` FOREIGN KEY (`magazine_id`) REFERENCES `magazines` (`id`) @@ -443,7 +444,7 @@ DROP TABLE IF EXISTS `magazines`; CREATE TABLE `magazines` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `title` text NOT NULL, - `notes` text NOT NULL, + `notes` text NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; From 4451123f1225ed98f7453d0b1caee1a97737a486 Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Tue, 26 May 2026 12:41:08 +0200 Subject: [PATCH 10/18] (feat) Add IP whitelisting --- CHANGELOG.md | 7 +++++++ app.py | 23 +++++++++++++++++++++++ configuration.json.dist | 3 ++- docs/API.md | 1 + docs/SETUP.md | 2 ++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c35036..fc9bb29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 5.0.0 +* Bump to Python 3.13. +* Migration to Poetry. +* Added strong typing. +* Added a feature to manage magazines. +* Added an optionnal whitelist of IP adresses. + ## 4.4.0 * Added a new feature: notes. * Bump to Python 3.10. diff --git a/app.py b/app.py index b3b8899..5708a15 100644 --- a/app.py +++ b/app.py @@ -27,6 +27,7 @@ with open('configuration.json', encoding='UTF-8') as json_file: configurationData = json.load(json_file) +authorized_ips = configurationData.get('authorized_ips', []) ################ # DB connection @@ -71,6 +72,28 @@ def decorator(*args: Any, **kwargs: Any) -> tuple[Response, int] | Any: return decorated_function(*args, **kwargs) return decorator +######################################################################## +# Before request: security... +######################################################################## + +@app.before_request +def restrict_ip_access() -> tuple[Response, int] | None: + """Restrict access to authorized IPs if a whitelist is configured.""" + + if not authorized_ips: + return None + + client_ip = request.headers.get('X-Forwarded-For', request.remote_addr) + client_ip = client_ip.split(',')[0].strip() + + if client_ip not in authorized_ips: + return jsonify({ + 'message': 'IP address is not authorized', + 'code': 16 + }), 403 + + return None + ######################################################################## # After request: cache management, close DB connection... ######################################################################## diff --git a/configuration.json.dist b/configuration.json.dist index 9f3e272..689f526 100644 --- a/configuration.json.dist +++ b/configuration.json.dist @@ -3,5 +3,6 @@ "db_user": "game", "db_password": "azerty", "database": "games", - "secret": "replaceThisSecret" + "secret": "replaceThisSecret", + "authorized_ips": [] } \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index 74cc5f5..60da90e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -106,3 +106,4 @@ When something goes wrong, we try to handle it with a specific exception and a s | 13 | Authentication token is invalid | | | 14 | Inconsistant version and copy | Raised when you try to create a transaction for which version_id and the copy version_id don't match | | 15 | Duplicate consecutive operation | Raised when you try, for instance, to create two consecutive inbound transaction | +| 16 | Unauthorized client | Returned when the client IP is not in the whitelist | diff --git a/docs/SETUP.md b/docs/SETUP.md index 5fb955a..84333a0 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -27,6 +27,8 @@ The usual process to setup a project is the following: * Start the application. * Change your default credentials. +Note: in the configuration, you can define a list of authorized IP adresses. It is recommended for production environment, but not for the dev one as it will prevent tests to pass. + ## 1- Importing the dabase Nothing much to say here, import the empty database in to your MySQL server. From a037dff0dbd7d3f071f49a026598ca4423763d60 Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Wed, 27 May 2026 20:57:28 +0200 Subject: [PATCH 11/18] fixes --- migrations/5.0.0.sql | 19 +++++++------------ src/entity/magazine_issue_copy.py | 4 ++-- test/games_test.sql | 24 +++--------------------- 3 files changed, 12 insertions(+), 35 deletions(-) diff --git a/migrations/5.0.0.sql b/migrations/5.0.0.sql index bb75826..9dd27ab 100644 --- a/migrations/5.0.0.sql +++ b/migrations/5.0.0.sql @@ -6,9 +6,6 @@ ALTER TABLE `copies` DROP FOREIGN KEY `copies_ibfk_1`; -- version_id versions(version_id) RESTRICT RESTRICT ALTER TABLE `stories` DROP FOREIGN KEY `stories_ibfk_1`; --- copy_id copies(copy_id) RESTRICT RESTRICT -ALTER TABLE `trades` DROP FOREIGN KEY `trades_ibfk_1`; - -- copy_id copies(copy_id) RESTRICT RESTRICT ALTER TABLE `transactions` DROP FOREIGN KEY `transactions_ibfk_1`; @@ -27,9 +24,6 @@ ALTER TABLE `copies` CHANGE `copy_id` `copy_id` int unsigned NOT NULL AUTO_INCREMENT FIRST, CHANGE `version_id` `version_id` int unsigned NOT NULL AFTER `copy_id`; -ALTER TABLE `trades` -CHANGE `copy_id` `copy_id` int unsigned NOT NULL AFTER `trade_id`; - ALTER TABLE `transactions` CHANGE `version_id` `version_id` int unsigned NOT NULL AFTER `transaction_id`, CHANGE `copy_id` `copy_id` int unsigned NULL AFTER `version_id`; @@ -47,8 +41,6 @@ ALTER TABLE `copies` ADD FOREIGN KEY (`version_id`) REFERENCES `versions` (`vers ALTER TABLE `stories` ADD FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) ON DELETE RESTRICT ON UPDATE RESTRICT; -ALTER TABLE `trades` ADD FOREIGN KEY (`copy_id`) REFERENCES `copies` (`copy_id`) ON DELETE RESTRICT ON UPDATE RESTRICT; - ALTER TABLE `transactions` ADD FOREIGN KEY (`copy_id`) REFERENCES `copies` (`copy_id`) ON DELETE RESTRICT ON UPDATE RESTRICT; ALTER TABLE `transactions` ADD FOREIGN KEY (`version_id`) REFERENCES `versions` (`version_id`) ON DELETE RESTRICT ON UPDATE RESTRICT; @@ -76,12 +68,15 @@ CREATE TABLE `magazine_issues` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `magazine_issue_copies` ( - `copy_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, - `magazine_issue_id` INT UNSIGNED NOT NULL, - `type` VARCHAR(255) NOT NULL, + `copy_id` int unsigned NOT NULL AUTO_INCREMENT, + `magazine_issue_id` int unsigned NOT NULL, + `type` varchar(255) NOT NULL, + `notes` text, PRIMARY KEY (`copy_id`), + KEY `fk_magazine_issue_copy_issue` (`magazine_issue_id`), CONSTRAINT `fk_magazine_issue_copy_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + CREATE TABLE `game_version_magazine_mentions` ( `mention_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, diff --git a/src/entity/magazine_issue_copy.py b/src/entity/magazine_issue_copy.py index 99fafd3..1a347cb 100644 --- a/src/entity/magazine_issue_copy.py +++ b/src/entity/magazine_issue_copy.py @@ -29,13 +29,13 @@ class MagazineIssueCopy(AbstractEntity): } authorized_extra_fields_for_filtering: dict[str, Any] = { - 'id': {'field': 'issue_copy_id', 'origin': 'native', 'type': 'int'}, + 'id': {'field': 'copy_id', 'origin': 'native', 'type': 'int'}, 'magazineIssueId': {'field': 'magazine_issue_id', 'origin': 'native', 'type': 'int'}, 'type': {'field': 'type', 'origin': 'native', 'type': 'string'}, } table_name = 'magazine_issue_copies' - primary_key = 'issue_copy_id' + primary_key = 'copy_id' # If you change the order here, you need to also change it in the array above! def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments diff --git a/test/games_test.sql b/test/games_test.sql index 9b5a406..022d47a 100644 --- a/test/games_test.sql +++ b/test/games_test.sql @@ -409,16 +409,16 @@ INSERT INTO `games` (`id`, `title`, `notes`) VALUES DROP TABLE IF EXISTS `magazine_issue_copies`; CREATE TABLE `magazine_issue_copies` ( - `issue_copy_id` int unsigned NOT NULL AUTO_INCREMENT, + `copy_id` int unsigned NOT NULL AUTO_INCREMENT, `magazine_issue_id` int unsigned NOT NULL, `type` varchar(255) NOT NULL, `notes` text, - PRIMARY KEY (`issue_copy_id`), + PRIMARY KEY (`copy_id`), KEY `fk_magazine_issue_copy_issue` (`magazine_issue_id`), CONSTRAINT `fk_magazine_issue_copy_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -INSERT INTO `magazine_issue_copies` (`issue_copy_id`, `magazine_issue_id`, `type`, `notes`) VALUES +INSERT INTO `magazine_issue_copies` (`copy_id`, `magazine_issue_id`, `type`, `notes`) VALUES (1, 1, 'Printed-Original', 'Original'), (2, 1, 'Digital', 'Copy.'); @@ -580,24 +580,6 @@ INSERT INTO `stories` (`id`, `version_id`, `year`, `position`, `watched`, `playe (89, 80, 2022, 13, 0, 1), (90, 224, 2022, 14, 0, 1); -DROP TABLE IF EXISTS `trades`; -CREATE TABLE `trades` ( - `trade_id` int unsigned NOT NULL AUTO_INCREMENT, - `copy_id` int unsigned NOT NULL, - `year` smallint unsigned NOT NULL, - `month` smallint unsigned NOT NULL, - `day` smallint unsigned NOT NULL, - `type` varchar(255) NOT NULL, - `notes` text, - PRIMARY KEY (`trade_id`), - KEY `copy_id` (`copy_id`), - CONSTRAINT `trades_ibfk_1` FOREIGN KEY (`copy_id`) REFERENCES `copies` (`copy_id`) ON DELETE RESTRICT ON UPDATE RESTRICT -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -INSERT INTO `trades` (`trade_id`, `copy_id`, `year`, `month`, `day`, `type`, `notes`) VALUES -(90, 1, 2022, 2, 4, 'Loan-out', ''), -(91, 1, 2022, 4, 8, 'Loan-out-return', ''); - DROP TABLE IF EXISTS `transactions`; CREATE TABLE `transactions` ( `transaction_id` int unsigned NOT NULL AUTO_INCREMENT, From 894f4ffd948ae2f3d328e5a6bb9e9578725ca7a5 Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Wed, 27 May 2026 21:04:14 +0200 Subject: [PATCH 12/18] Other fixes on the migration --- migrations/5.0.0.sql | 2 +- test/games_test.sql | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/migrations/5.0.0.sql b/migrations/5.0.0.sql index 9dd27ab..c831650 100644 --- a/migrations/5.0.0.sql +++ b/migrations/5.0.0.sql @@ -75,7 +75,7 @@ CREATE TABLE `magazine_issue_copies` ( PRIMARY KEY (`copy_id`), KEY `fk_magazine_issue_copy_issue` (`magazine_issue_id`), CONSTRAINT `fk_magazine_issue_copy_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `game_version_magazine_mentions` ( diff --git a/test/games_test.sql b/test/games_test.sql index 022d47a..4043d64 100644 --- a/test/games_test.sql +++ b/test/games_test.sql @@ -416,7 +416,7 @@ CREATE TABLE `magazine_issue_copies` ( PRIMARY KEY (`copy_id`), KEY `fk_magazine_issue_copy_issue` (`magazine_issue_id`), CONSTRAINT `fk_magazine_issue_copy_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `magazine_issue_copies` (`copy_id`, `magazine_issue_id`, `type`, `notes`) VALUES (1, 1, 'Printed-Original', 'Original'), @@ -433,7 +433,7 @@ CREATE TABLE `magazine_issues` ( PRIMARY KEY (`id`), KEY `fk_magazine_issue_magazine` (`magazine_id`), CONSTRAINT `fk_magazine_issue_magazine` FOREIGN KEY (`magazine_id`) REFERENCES `magazines` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `magazine_issues` (`id`, `magazine_id`, `issue_number`, `year`, `month`, `notes`) VALUES (1, 1, 3, 1997, 10, 'Le troisième !'), @@ -446,7 +446,7 @@ CREATE TABLE `magazines` ( `title` text NOT NULL, `notes` text NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `magazines` (`id`, `title`, `notes`) VALUES (1, 'Gen4', 'Découvert en 1997.'), @@ -458,7 +458,7 @@ CREATE TABLE `notes` ( `title` varchar(255) NOT NULL, `content` text, PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `notes` (`id`, `title`, `content`) VALUES (1, 'Note 1', 'Some comment 1.'), From ae1ee6e8b1e776040d646592e12b554e90633c2e Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Thu, 28 May 2026 13:14:19 +0200 Subject: [PATCH 13/18] (feat): Add new mention type --- src/entity/game_version_magazine_mention.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/entity/game_version_magazine_mention.py b/src/entity/game_version_magazine_mention.py index cf720f7..64ba3bc 100644 --- a/src/entity/game_version_magazine_mention.py +++ b/src/entity/game_version_magazine_mention.py @@ -25,7 +25,9 @@ class GameVersionMagazineMention(AbstractEntity): 'type': 'strict-text', 'allowed_values': { 'Advertisement', + 'Cheat', 'Comparison', + 'Full-game-included' 'Guide', 'Mention', 'Playable-demo', From 315ab7195388f4c15240cd0bf116c820694e73cc Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Thu, 28 May 2026 21:45:35 +0200 Subject: [PATCH 14/18] Fix magazines types --- src/entity/game_version_magazine_mention.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/entity/game_version_magazine_mention.py b/src/entity/game_version_magazine_mention.py index 64ba3bc..6dc368a 100644 --- a/src/entity/game_version_magazine_mention.py +++ b/src/entity/game_version_magazine_mention.py @@ -27,14 +27,14 @@ class GameVersionMagazineMention(AbstractEntity): 'Advertisement', 'Cheat', 'Comparison', - 'Full-game-included' + 'Full-game-included', 'Guide', 'Mention', 'Playable-demo', 'Other', 'Preview', - 'Short preview', - 'Short test', + 'Short-preview', + 'Short-test', 'Test', 'Watchable-demo', } From 1019b144e1c526ebd560567a9d42a361b49ae050 Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Thu, 28 May 2026 22:16:26 +0200 Subject: [PATCH 15/18] (feat) Magazine: add page number --- migrations/5.0.0.sql | 2 +- src/entity/game_version_magazine_mention.py | 14 ++++++++++++++ .../test_game_version_magazine_mentions.py | 8 ++++---- test/games_test.sql | 9 +++++---- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/migrations/5.0.0.sql b/migrations/5.0.0.sql index c831650..7b8a430 100644 --- a/migrations/5.0.0.sql +++ b/migrations/5.0.0.sql @@ -77,12 +77,12 @@ CREATE TABLE `magazine_issue_copies` ( CONSTRAINT `fk_magazine_issue_copy_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - CREATE TABLE `game_version_magazine_mentions` ( `mention_id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `magazine_issue_id` INT UNSIGNED NOT NULL, `game_version_id` INT UNSIGNED NOT NULL, `type` VARCHAR(255) NOT NULL, + `page_number` int unsigned NOT NULL, `notes` TEXT NOT NULL, PRIMARY KEY (`mention_id`), CONSTRAINT `fk_mention_magazine_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`), diff --git a/src/entity/game_version_magazine_mention.py b/src/entity/game_version_magazine_mention.py index 6dc368a..07c6873 100644 --- a/src/entity/game_version_magazine_mention.py +++ b/src/entity/game_version_magazine_mention.py @@ -39,6 +39,12 @@ class GameVersionMagazineMention(AbstractEntity): 'Watchable-demo', } }, + 'pageNumber': { + 'field': 'page_number', + 'method': '_page_number', + 'required': True, + 'type': 'int' + }, 'notes': { 'field': 'notes', 'method': '_notes', @@ -64,12 +70,14 @@ def __init__( # pylint: disable=too-many-arguments,too-many-positional-argument magazine_issue_id: int, game_version_id: int, type: str, + page_number: int, notes: str ) -> None: self.entity_id = entity_id self.magazine_issue_id = int(magazine_issue_id) self.game_version_id = int(game_version_id) self.type = type + self.page_number = page_number self.notes = notes def get_id(self) -> int | None: @@ -93,6 +101,12 @@ def get_type(self) -> str: def set_type(self, type: str) -> None: self.type = type + def get_page_number(self) -> int: + return self.page_number + + def set_page_numner(self, page_number: int) -> None: + self.page_number = page_number + def get_notes(self) -> str: return self.notes diff --git a/test/functional/test_game_version_magazine_mentions.py b/test/functional/test_game_version_magazine_mentions.py index f199d61..b758746 100644 --- a/test/functional/test_game_version_magazine_mentions.py +++ b/test/functional/test_game_version_magazine_mentions.py @@ -16,7 +16,7 @@ def test_get_mention(self): resp = self.api_call('get', 'game-version-magazine-mention/1', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual({'id': 1, 'magazineIssueId': 2, 'gameVersionId': 1, 'type': 'Test', 'notes': ''}, resp.json()) + self.assertEqual({'id': 1, 'magazineIssueId': 2, 'gameVersionId': 1, 'type': 'Test', 'pageNumber': 12, 'notes': ''}, resp.json()) def test_create_incomplete_payload(self): resp = self.api_call('post', 'game-version-magazine-mention', {}, True) @@ -30,20 +30,20 @@ def test_create_unsupported_type(self): self.assertEqual(400, resp.status_code) def test_create_magazine_issue_not_found(self): - resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 666, 'gameVersionId': 1, 'type': 'Test'}, True) + resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 666, 'gameVersionId': 1, 'type': 'Test', 'pageNumber': 49}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) def test_create_game_version_not_found(self): - resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 1, 'gameVersionId': 666, 'type': 'Test'}, True) + resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 1, 'gameVersionId': 666, 'type': 'Test', 'pageNumber': 49}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'version' with id #666 has not been found.", 'code': 1}, resp.json()) def test_create_update_delete_success(self): # Create - payload = {'magazineIssueId': 1, 'gameVersionId': 1, 'type': 'Preview', 'notes': 'A preview mention.'} + payload = {'magazineIssueId': 1, 'gameVersionId': 1, 'type': 'Preview', 'pageNumber': 49, 'notes': 'A preview mention.'} resp = self.api_call('post', 'game-version-magazine-mention', payload, True) self.assertEqual(200, resp.status_code) diff --git a/test/games_test.sql b/test/games_test.sql index 4043d64..01de2f4 100644 --- a/test/games_test.sql +++ b/test/games_test.sql @@ -41,6 +41,7 @@ CREATE TABLE `game_version_magazine_mentions` ( `magazine_issue_id` int unsigned NOT NULL, `game_version_id` int unsigned NOT NULL, `type` varchar(255) NOT NULL, + `page_number` int unsigned NOT NULL, `notes` text NULL, PRIMARY KEY (`mention_id`), KEY `fk_mention_magazine_issue` (`magazine_issue_id`), @@ -49,10 +50,10 @@ CREATE TABLE `game_version_magazine_mentions` ( CONSTRAINT `fk_mention_magazine_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -INSERT INTO `game_version_magazine_mentions` (`mention_id`, `magazine_issue_id`, `game_version_id`, `type`, `notes`) VALUES -(1, 2, 1, 'Test', ''), -(2, 1, 1, 'Guide', ''), -(3, 1, 36, 'Test', 'Super !'); +INSERT INTO `game_version_magazine_mentions` (`mention_id`, `magazine_issue_id`, `game_version_id`, `type`, `page_number`, `notes`) VALUES +(1, 2, 1,'Test', 12, ''), +(2, 1, 1, 'Guide', 36, ''), +(3, 1, 36, 'Test', 43, 'Super !'); DROP TABLE IF EXISTS `games`; CREATE TABLE `games` ( From b264e48212ea3a885d037425217d7584805c2b4e Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Mon, 1 Jun 2026 13:22:17 +0200 Subject: [PATCH 16/18] (feat): Add magazine sorting by page number --- src/entity/game_version_magazine_mention.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/entity/game_version_magazine_mention.py b/src/entity/game_version_magazine_mention.py index 07c6873..d55b86f 100644 --- a/src/entity/game_version_magazine_mention.py +++ b/src/entity/game_version_magazine_mention.py @@ -57,6 +57,7 @@ class GameVersionMagazineMention(AbstractEntity): 'id': {'field': 'mention_id', 'origin': 'native', 'type': 'int'}, 'magazineIssueId': {'field': 'magazine_issue_id', 'origin': 'native', 'type': 'int'}, 'gameVersionId': {'field': 'game_version_id', 'origin': 'native', 'type': 'int'}, + 'pageNumber': {'field': 'game_version_id', 'origin': 'native', 'type': 'int'}, 'type': {'field': 'type', 'origin': 'native', 'type': 'string'}, } From c696fef08b1443a47e33b8b0b700827833f59bd7 Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Thu, 18 Jun 2026 08:22:38 +0200 Subject: [PATCH 17/18] (feat): the user can link a note to a game version --- CHANGELOG.md | 2 ++ migrations/5.0.0.sql | 3 +++ src/controller/abstract_controller.py | 4 ++-- src/entity/note.py | 15 ++++++++++++++- src/repository/abstract_repository.py | 10 +++++++++- test/functional/test_notes.py | 2 +- test/games_test.sql | 5 ++++- 7 files changed, 35 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9bb29..f14b26f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * Added strong typing. * Added a feature to manage magazines. * Added an optionnal whitelist of IP adresses. +* We can link a note to a game version. +* API filters: for filter of type int, we can check against NULL by sending "Null" as the parameter. ## 4.4.0 * Added a new feature: notes. diff --git a/migrations/5.0.0.sql b/migrations/5.0.0.sql index 7b8a430..907a221 100644 --- a/migrations/5.0.0.sql +++ b/migrations/5.0.0.sql @@ -88,3 +88,6 @@ CREATE TABLE `game_version_magazine_mentions` ( CONSTRAINT `fk_mention_magazine_issue` FOREIGN KEY (`magazine_issue_id`) REFERENCES `magazine_issues` (`id`), CONSTRAINT `fk_mention_game_version` FOREIGN KEY (`game_version_id`) REFERENCES `versions` (`version_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +ALTER TABLE notes ADD game_version_id INT UNSIGNED NULL; +ALTER TABLE `notes` ADD FOREIGN KEY (`game_version_id`) REFERENCES `versions` (`version_id`); diff --git a/src/controller/abstract_controller.py b/src/controller/abstract_controller.py index c2e64c5..3c1e408 100644 --- a/src/controller/abstract_controller.py +++ b/src/controller/abstract_controller.py @@ -13,11 +13,11 @@ def get_by_id(cls, mysql: Any, entity_id: int) -> tuple[Response, int]: service = cls.service(mysql) try: - copy = service.get_by_id(entity_id) + object = service.get_by_id(entity_id) except ResourceNotFoundException as error: return jsonify({'message': str(error), 'code': error.get_code()}), 404 - return jsonify(copy.serialize()), 200 + return jsonify(object.serialize()), 200 @classmethod def create(cls, mysql: Any) -> tuple[Response, int]: diff --git a/src/entity/note.py b/src/entity/note.py index 72953be..85bcb50 100644 --- a/src/entity/note.py +++ b/src/entity/note.py @@ -21,15 +21,22 @@ class Note(AbstractEntity): 'type': 'text', 'default': '' }, + 'gameVersionId': { + 'field': 'game_version_id', + 'method': '_game_version_id', + 'required': False, + 'type': 'int' + }, } table_name = 'notes' primary_key = 'id' - def __init__(self, entity_id: int | None, title: str, content: str) -> None: + def __init__(self, entity_id: int | None, title: str, content: str, game_version_id: int) -> None: self.entity_id = entity_id self.title = title self.content = content + self.game_version_id = game_version_id def get_id(self) -> int | None: return self.entity_id @@ -40,8 +47,14 @@ def get_title(self) -> str: def get_content(self) -> str: return self.content + def get_game_version_id(self) -> int|None: + return self.game_version_id + def set_title(self, title: str) -> None: self.title = title def set_content(self, content: str) -> None: self.content = content + + def set_game_version_id(self, game_version_id: int)-> None: + self.game_version_id = game_version_id diff --git a/src/repository/abstract_repository.py b/src/repository/abstract_repository.py index 9fbf4b5..2566628 100644 --- a/src/repository/abstract_repository.py +++ b/src/repository/abstract_repository.py @@ -116,10 +116,18 @@ def create_get_list_filter_condition(cls, current_filter_values: list[str], filt for filter_value in current_filter_values: # Various possibility according to the field type if filter_data['type'] == 'int' or filter_data['type'] == 'strict-text': - comparison_operator = ' = ' + comparison_operator = '=' if filter_data['type'] == 'int': for comp_url_key, comp_sql_key in comparison_operators.items(): # pylint: disable=W0612 + # Special case for Null + if filter_value == 'Null' and comparison_operator == '=': + or_request += field + " IS NULL OR " + continue + if filter_value == 'Null': + or_request += field + " IS NOT NULL OR " + continue + if filter_value.startswith(comp_url_key + '-'): array = filter_value.split('-') comparison_operator = comparison_operators[array[0]] diff --git a/test/functional/test_notes.py b/test/functional/test_notes.py index 1c39f6f..cf8e699 100644 --- a/test/functional/test_notes.py +++ b/test/functional/test_notes.py @@ -16,7 +16,7 @@ def test_get_note(self): resp = self.api_call('get', 'note/1', {}, True) self.assertEqual(200, resp.status_code) - self.assertEqual({'id': 1, 'title': 'Note 1', 'content': 'Some comment 1.'}, resp.json()) + self.assertEqual({'id': 1, 'title': 'Note 1', 'content': 'Some comment 1.', 'gameVersionId': None}, resp.json()) def test_create_incomplete_payload(self): resp = self.api_call('post', 'note', {}, True) diff --git a/test/games_test.sql b/test/games_test.sql index 01de2f4..13d3c5f 100644 --- a/test/games_test.sql +++ b/test/games_test.sql @@ -458,12 +458,15 @@ CREATE TABLE `notes` ( `id` smallint unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `content` text, + `game_version_id` INT UNSIGNED NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +ALTER TABLE `notes` ADD FOREIGN KEY (`game_version_id`) REFERENCES `versions` (`version_id`); + INSERT INTO `notes` (`id`, `title`, `content`) VALUES (1, 'Note 1', 'Some comment 1.'), -(2, 'Note 2', 'Some comment 2'); +(2, 'Note 2', 'Some comment 2.'); DROP TABLE IF EXISTS `platforms`; CREATE TABLE `platforms` ( From 9e97525fbbe95f610d0ce55d55cfd68e6ccb746d Mon Sep 17 00:00:00 2001 From: Eric COURTIAL Date: Mon, 27 Jul 2026 13:17:49 +0200 Subject: [PATCH 18/18] (feat): resource names are now plural --- CHANGELOG.md | 2 + app.py | 98 +++++++++---------- test/functional/test_copies.py | 34 +++---- .../test_game_version_magazine_mentions.py | 28 +++--- test/functional/test_games.py | 28 +++--- test/functional/test_magazine_issue_copies.py | 26 ++--- test/functional/test_magazine_issues.py | 36 +++---- test/functional/test_magazines.py | 30 +++--- test/functional/test_notes.py | 18 ++-- test/functional/test_platforms.py | 26 ++--- test/functional/test_stories.py | 26 ++--- test/functional/test_transactions.py | 78 +++++++-------- test/functional/test_users.py | 40 ++++---- test/functional/test_versions.py | 44 ++++----- 14 files changed, 258 insertions(+), 256 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f14b26f..5f4acf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ * We can link a note to a game version. * API filters: for filter of type int, we can check against NULL by sending "Null" as the parameter. +BC Break: the URI for resources are now plural. + ## 4.4.0 * Added a new feature: notes. * Bump to Python 3.10. diff --git a/app.py b/app.py index 5708a15..819af43 100644 --- a/app.py +++ b/app.py @@ -122,13 +122,13 @@ def home() -> tuple[Response, int]: # Users -@app.route('/api/v1/user/authenticate', methods=['POST']) +@app.route('/api/v1/users/authenticate', methods=['POST']) def authenticate_user() -> tuple[Response, int]: """Returns the token of the given user""" controller = UserController return controller.authenticate(MySQLFactory.get()) -@app.route('/api/v1/user', methods=['GET']) +@app.route('/api/v1/users', methods=['GET']) @token_required def get_user() -> tuple[Response, int]: """Returns the user according to one filter""" @@ -139,21 +139,21 @@ def get_user() -> tuple[Response, int]: request.args.get('value', '') ) -@app.route('/api/v1/user', methods=['POST']) +@app.route('/api/v1/users', methods=['POST']) @token_required def create_user() -> tuple[Response, int]: """Creates a user""" controller = UserController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/user/', methods=['PATCH']) +@app.route('/api/v1/users/', methods=['PATCH']) @token_required def update_user(entity_id: int) -> tuple[Response, int]: """Updates a user""" controller = UserController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/user/renew-token', methods=['POST']) +@app.route('/api/v1/users/renew-token', methods=['POST']) @token_required def renew_token(current_user: Any) -> tuple[Response, int]: """Renew the API token of the current user""" @@ -162,27 +162,27 @@ def renew_token(current_user: Any) -> tuple[Response, int]: # Platforms -@app.route('/api/v1/platform/', methods=['GET']) +@app.route('/api/v1/platforms/', methods=['GET']) def get_platform_by_id(entity_id: int) -> tuple[Response, int]: """Returns the platform according to its id""" controller = PlatformController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/platform', methods=['POST']) +@app.route('/api/v1/platforms', methods=['POST']) @token_required def create_platform() -> tuple[Response, int]: """Create a platform""" controller = PlatformController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/platform/', methods=['PATCH']) +@app.route('/api/v1/platforms/', methods=['PATCH']) @token_required def update_platform(entity_id: int) -> tuple[Response, int]: """Update the platform according to its id""" controller = PlatformController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/platform/', methods=['DELETE']) +@app.route('/api/v1/platforms/', methods=['DELETE']) @token_required def delete_platform(entity_id: int) -> tuple[Response, int]: """Delete the platform according to its id""" @@ -197,27 +197,27 @@ def get_platforms() -> Response: # Games -@app.route('/api/v1/game/', methods=['GET']) +@app.route('/api/v1/games/', methods=['GET']) def get_game_by_id(entity_id: int) -> tuple[Response, int]: """Returns the game according to its id""" controller = GameController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/game', methods=['POST']) +@app.route('/api/v1/games', methods=['POST']) @token_required def create_game() -> tuple[Response, int]: """Create a game""" controller = GameController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/game/', methods=['PATCH']) +@app.route('/api/v1/games/', methods=['PATCH']) @token_required def update_game(entity_id: int) -> tuple[Response, int]: """Update the game according to its id""" controller = GameController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/game/', methods=['DELETE']) +@app.route('/api/v1/games/', methods=['DELETE']) @token_required def delete_game(entity_id: int) -> tuple[Response, int]: """Delete the game according to its id""" @@ -232,27 +232,27 @@ def get_games() -> Response: # Versions -@app.route('/api/v1/version/', methods=['GET']) +@app.route('/api/v1/versions/', methods=['GET']) def get_version_by_id(entity_id: int) -> tuple[Response, int]: """Returns the version according to its id""" controller = VersionController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/version', methods=['POST']) +@app.route('/api/v1/versions', methods=['POST']) @token_required def create_version() -> tuple[Response, int]: """Create a version""" controller = VersionController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/version/', methods=['PATCH']) +@app.route('/api/v1/versions/', methods=['PATCH']) @token_required def update_version(entity_id: int) -> tuple[Response, int]: """Update the version according to its id""" controller = VersionController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/version/', methods=['DELETE']) +@app.route('/api/v1/versions/', methods=['DELETE']) @token_required def delete_version(entity_id: int) -> tuple[Response, int]: """Delete the version according to its id""" @@ -267,27 +267,27 @@ def get_versions() -> Response: # Copies -@app.route('/api/v1/copy/', methods=['GET']) +@app.route('/api/v1/copies/', methods=['GET']) def get_copy_by_id(entity_id: int) -> tuple[Response, int]: """Returns the copy according to its id""" controller = CopyController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/copy', methods=['POST']) +@app.route('/api/v1/copies', methods=['POST']) @token_required def create_copy() -> tuple[Response, int]: """Create a copy""" controller = CopyController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/copy/', methods=['PATCH']) +@app.route('/api/v1/copies/', methods=['PATCH']) @token_required def update_copy(entity_id: int) -> tuple[Response, int]: """Update the copy according to its id""" controller = CopyController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/copy/', methods=['DELETE']) +@app.route('/api/v1/copies/', methods=['DELETE']) @token_required def delete_copy(entity_id: int) -> tuple[Response, int]: """Delete the copy according to its id""" @@ -302,27 +302,27 @@ def get_copies() -> Response: # Stories -@app.route('/api/v1/story/', methods=['GET']) +@app.route('/api/v1/stories/', methods=['GET']) def get_story_by_id(entity_id: int) -> tuple[Response, int]: """Returns the story according to its id""" controller = StoryController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/story', methods=['POST']) +@app.route('/api/v1/stories', methods=['POST']) @token_required def create_story() -> tuple[Response, int]: """Create a story""" controller = StoryController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/story/', methods=['PATCH']) +@app.route('/api/v1/stories/', methods=['PATCH']) @token_required def update_story(entity_id: int) -> tuple[Response, int]: """Update the story according to its id""" controller = StoryController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/story/', methods=['DELETE']) +@app.route('/api/v1/stories/', methods=['DELETE']) @token_required def delete_story(entity_id: int) -> tuple[Response, int]: """Delete the story according to its id""" @@ -337,27 +337,27 @@ def get_stories() -> Response: # Transactions -@app.route('/api/v1/transaction/', methods=['GET']) +@app.route('/api/v1/transactions/', methods=['GET']) def get_transaction_by_id(entity_id: int) -> tuple[Response, int]: """Returns the transaction according to its id""" controller = TransactionController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/transaction', methods=['POST']) +@app.route('/api/v1/transactions', methods=['POST']) @token_required def create_transaction() -> tuple[Response, int]: """Create a transaction""" controller = TransactionController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/transaction/', methods=['PATCH']) +@app.route('/api/v1/transactions/', methods=['PATCH']) @token_required def update_transaction(entity_id: int) -> tuple[Response, int]: """Update the transaction according to its id""" controller = TransactionController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/transaction/', methods=['DELETE']) +@app.route('/api/v1/transactions/', methods=['DELETE']) @token_required def delete_transaction(entity_id: int) -> tuple[Response, int]: """Delete the transaction according to its id""" @@ -372,27 +372,27 @@ def get_transactions() -> Response: # Notes -@app.route('/api/v1/note/', methods=['GET']) +@app.route('/api/v1/notes/', methods=['GET']) def get_note_by_id(entity_id: int) -> tuple[Response, int]: """Returns the note according to its id""" controller = NoteController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/note', methods=['POST']) +@app.route('/api/v1/notes', methods=['POST']) @token_required def create_note() -> tuple[Response, int]: """Create a note""" controller = NoteController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/note/', methods=['PATCH']) +@app.route('/api/v1/notes/', methods=['PATCH']) @token_required def update_note(entity_id: int) -> tuple[Response, int]: """Update the note according to its id""" controller = NoteController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/note/', methods=['DELETE']) +@app.route('/api/v1/notes/', methods=['DELETE']) @token_required def delete_note(entity_id: int) -> tuple[Response, int]: """Delete the note according to its id""" @@ -407,27 +407,27 @@ def get_notes() -> Response: # Magazines -@app.route('/api/v1/magazine/', methods=['GET']) +@app.route('/api/v1/magazines/', methods=['GET']) def get_magazine_by_id(entity_id: int) -> tuple[Response, int]: """Returns the magazine according to its id""" controller = MagazineController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/magazine', methods=['POST']) +@app.route('/api/v1/magazines', methods=['POST']) @token_required def create_magazine() -> tuple[Response, int]: """Create a magazine""" controller = MagazineController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/magazine/', methods=['PATCH']) +@app.route('/api/v1/magazines/', methods=['PATCH']) @token_required def update_magazine(entity_id: int) -> tuple[Response, int]: """Update the magazine according to its id""" controller = MagazineController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/magazine/', methods=['DELETE']) +@app.route('/api/v1/magazines/', methods=['DELETE']) @token_required def delete_magazine(entity_id: int) -> tuple[Response, int]: """Delete the magazine according to its id""" @@ -442,27 +442,27 @@ def get_magazines() -> Response: # Magazine Issues -@app.route('/api/v1/magazine-issue/', methods=['GET']) +@app.route('/api/v1/magazine-issues/', methods=['GET']) def get_magazine_issue_by_id(entity_id: int) -> tuple[Response, int]: """Returns the magazine issue according to its id""" controller = MagazineIssueController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/magazine-issue', methods=['POST']) +@app.route('/api/v1/magazine-issues', methods=['POST']) @token_required def create_magazine_issue() -> tuple[Response, int]: """Create a magazine issue""" controller = MagazineIssueController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/magazine-issue/', methods=['PATCH']) +@app.route('/api/v1/magazine-issues/', methods=['PATCH']) @token_required def update_magazine_issue(entity_id: int) -> tuple[Response, int]: """Update the magazine issue according to its id""" controller = MagazineIssueController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/magazine-issue/', methods=['DELETE']) +@app.route('/api/v1/magazine-issues/', methods=['DELETE']) @token_required def delete_magazine_issue(entity_id: int) -> tuple[Response, int]: """Delete the magazine issue according to its id""" @@ -477,27 +477,27 @@ def get_magazine_issues() -> Response: # Magazine Issue Copies -@app.route('/api/v1/magazine-issue-copy/', methods=['GET']) +@app.route('/api/v1/magazine-issue-copies/', methods=['GET']) def get_magazine_issue_copy_by_id(entity_id: int) -> tuple[Response, int]: """Returns the magazine issue copy according to its id""" controller = MagazineIssueCopyController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/magazine-issue-copy', methods=['POST']) +@app.route('/api/v1/magazine-issue-copies', methods=['POST']) @token_required def create_magazine_issue_copy() -> tuple[Response, int]: """Create a magazine issue copy""" controller = MagazineIssueCopyController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/magazine-issue-copy/', methods=['PATCH']) +@app.route('/api/v1/magazine-issue-copies/', methods=['PATCH']) @token_required def update_magazine_issue_copy(entity_id: int) -> tuple[Response, int]: """Update the magazine issue copy according to its id""" controller = MagazineIssueCopyController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/magazine-issue-copy/', methods=['DELETE']) +@app.route('/api/v1/magazine-issue-copies/', methods=['DELETE']) @token_required def delete_magazine_issue_copy(entity_id: int) -> tuple[Response, int]: """Delete the magazine issue copy according to its id""" @@ -512,27 +512,27 @@ def get_magazine_issue_copies() -> Response: # Game Version Magazine Mentions -@app.route('/api/v1/game-version-magazine-mention/', methods=['GET']) +@app.route('/api/v1/game-version-magazine-mentions/', methods=['GET']) def get_game_version_magazine_mention_by_id(entity_id: int) -> tuple[Response, int]: """Returns the game version magazine mention according to its id""" controller = GameVersionMagazineMentionController return controller.get_by_id(MySQLFactory.get(), entity_id) -@app.route('/api/v1/game-version-magazine-mention', methods=['POST']) +@app.route('/api/v1/game-version-magazine-mentions', methods=['POST']) @token_required def create_game_version_magazine_mention() -> tuple[Response, int]: """Create a game version magazine mention""" controller = GameVersionMagazineMentionController return controller.create(MySQLFactory.get()) -@app.route('/api/v1/game-version-magazine-mention/', methods=['PATCH']) +@app.route('/api/v1/game-version-magazine-mentions/', methods=['PATCH']) @token_required def update_game_version_magazine_mention(entity_id: int) -> tuple[Response, int]: """Update the game version magazine mention according to its id""" controller = GameVersionMagazineMentionController return controller.update(MySQLFactory.get(), entity_id) -@app.route('/api/v1/game-version-magazine-mention/', methods=['DELETE']) +@app.route('/api/v1/game-version-magazine-mentions/', methods=['DELETE']) @token_required def delete_game_version_magazine_mention(entity_id: int) -> tuple[Response, int]: """Delete the game version magazine mention according to its id""" diff --git a/test/functional/test_copies.py b/test/functional/test_copies.py index 9cd8048..d8134d9 100644 --- a/test/functional/test_copies.py +++ b/test/functional/test_copies.py @@ -2,8 +2,8 @@ class TestCopies(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('copy') - super().check_all_routes_error_missing_user_token('copy') + super().check_all_routes_error_bad_user_token('copies') + super().check_all_routes_error_missing_user_token('copies') def test_create_incomplete_payload(self): payload = { @@ -16,7 +16,7 @@ def test_create_incomplete_payload(self): "status": "In", "comments": "Found it somewhere" } - resp = self.api_call('post', 'copy', payload, True) + resp = self.api_call('post', 'copies', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: versionId.', 'code': 6}, resp.json()) @@ -38,7 +38,7 @@ def test_create_fails_version_not_found(self): 'region': 'PAL', "comments": "Found it somewhere" } - resp = self.api_call('post', 'copy', payload, True) + resp = self.api_call('post', 'copies', payload, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'version' with id #9999 has not been found.", 'code': 1}, resp.json()) @@ -59,7 +59,7 @@ def test_create_invalid_types(self): "comments": "Found it somewhere" } - resp = self.api_call('post', 'copy', payload, True) + resp = self.api_call('post', 'copies', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The field 'boxType' does not support the value 'Big boxe'. Supported values are: Big box, Cartridge box, Medium box, None, Other, Special box.", 'code': 11}, resp.json()) @@ -80,7 +80,7 @@ def test_create_invalid_casing_type(self): "comments": "Found it somewhere" } - resp = self.api_call('post', 'copy', payload, True) + resp = self.api_call('post', 'copies', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The field 'casingType' does not support the value 'CD-likesss'. Supported values are: CD-like, Cardboard sleeve, DVD-like, None, Other, Paper Sleeve, Plastic Sleeve, Plastic tube.", 'code': 11}, resp.json()) @@ -88,13 +88,13 @@ def test_create_invalid_casing_type(self): def test_get_copy(self): # Does not exist - resp = self.api_call('get', 'copy/666', {}, True) + resp = self.api_call('get', 'copies/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'copy' with id #666 has not been found.", 'code': 1}, resp.json()) # Exist - resp = self.api_call('get', 'copy/1', {}, True) + resp = self.api_call('get', 'copies/1', {}, True) expectedPayload = { "id": 1, @@ -143,7 +143,7 @@ def test_create_update_delete_success(self): 'transactionCount': 0 } - resp = self.api_call('post', 'copy', payload, True) + resp = self.api_call('post', 'copies', payload, True) self.assertEqual(200, resp.status_code) copy_id = str(resp.json()["id"]) @@ -152,7 +152,7 @@ def test_create_update_delete_success(self): payload['isBoxRepro'] = True self.assertEqual(payload, resp.json()) - resp = self.api_call('get', 'copy/' + str(copy_id), None, True) + resp = self.api_call('get', 'copies/' + str(copy_id), None, True) self.assertEqual(payload, resp.json()) # Patch @@ -177,7 +177,7 @@ def test_create_update_delete_success(self): 'region': 'PAL', } - resp = self.api_call('patch', 'copy/' + copy_id, payload, True) + resp = self.api_call('patch', 'copies/' + copy_id, payload, True) payload['id'] = int(copy_id) payload['isROM'] = True @@ -185,15 +185,15 @@ def test_create_update_delete_success(self): self.assertEqual(payload, resp.json()) # Delete - resp = self.api_call('delete', 'copy/' + copy_id, {}, True) + resp = self.api_call('delete', 'copies/' + copy_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'copy/' + copy_id, {}, True) + resp = self.api_call('delete', 'copies/' + copy_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': f"The resource of type 'copy' with id #{copy_id} has not been found.", 'code': 1}, resp.json()) def test_update_fails_because_resource_not_found(self): - resp = self.api_call('patch', 'copy/9999', None, True) + resp = self.api_call('patch', 'copies/9999', None, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'copy' with id #9999 has not been found.", 'code': 1}, resp.json()) @@ -212,19 +212,19 @@ def test_update_fails_invalid_types(self): "comments": "Found it somewhere" } - resp = self.api_call('patch', 'copy/1', payload, True) + resp = self.api_call('patch', 'copies/1', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The field 'boxType' does not support the value 'Big boxe'. Supported values are: Big box, Cartridge box, Medium box, None, Other, Special box.", 'code': 11}, resp.json()) def test_delete_fails_because_not_found(self): - resp = self.api_call('delete', 'copy/9999', None, True) + resp = self.api_call('delete', 'copies/9999', None, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'copy' with id #9999 has not been found.", 'code': 1}, resp.json()) def test_delete_fails_because_has_transactions(self): - resp = self.api_call('delete', 'copy/1', None, True) + resp = self.api_call('delete', 'copies/1', None, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The following resource type 'copy' has children of type 'transaction', so it cannot be deleted.", 'code': 9}, resp.json()) diff --git a/test/functional/test_game_version_magazine_mentions.py b/test/functional/test_game_version_magazine_mentions.py index b758746..798141e 100644 --- a/test/functional/test_game_version_magazine_mentions.py +++ b/test/functional/test_game_version_magazine_mentions.py @@ -2,41 +2,41 @@ class TestGameVersionMagazineMentions(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('game-version-magazine-mention') - super().check_all_routes_error_missing_user_token('game-version-magazine-mention') + super().check_all_routes_error_bad_user_token('game-version-magazine-mentions') + super().check_all_routes_error_missing_user_token('game-version-magazine-mentions') def test_get_mention(self): # Does not exist - resp = self.api_call('get', 'game-version-magazine-mention/666', {}, True) + resp = self.api_call('get', 'game-version-magazine-mentions/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'game_version_magazine_mention' with id #666 has not been found.", 'code': 1}, resp.json()) # Exists - resp = self.api_call('get', 'game-version-magazine-mention/1', {}, True) + resp = self.api_call('get', 'game-version-magazine-mentions/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'id': 1, 'magazineIssueId': 2, 'gameVersionId': 1, 'type': 'Test', 'pageNumber': 12, 'notes': ''}, resp.json()) def test_create_incomplete_payload(self): - resp = self.api_call('post', 'game-version-magazine-mention', {}, True) + resp = self.api_call('post', 'game-version-magazine-mentions', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: magazineIssueId.', 'code': 6}, resp.json()) def test_create_unsupported_type(self): - resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 1, 'gameVersionId': 1, 'type': 'Review'}, True) + resp = self.api_call('post', 'game-version-magazine-mentions', {'magazineIssueId': 1, 'gameVersionId': 1, 'type': 'Review'}, True) self.assertEqual(400, resp.status_code) def test_create_magazine_issue_not_found(self): - resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 666, 'gameVersionId': 1, 'type': 'Test', 'pageNumber': 49}, True) + resp = self.api_call('post', 'game-version-magazine-mentions', {'magazineIssueId': 666, 'gameVersionId': 1, 'type': 'Test', 'pageNumber': 49}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) def test_create_game_version_not_found(self): - resp = self.api_call('post', 'game-version-magazine-mention', {'magazineIssueId': 1, 'gameVersionId': 666, 'type': 'Test', 'pageNumber': 49}, True) + resp = self.api_call('post', 'game-version-magazine-mentions', {'magazineIssueId': 1, 'gameVersionId': 666, 'type': 'Test', 'pageNumber': 49}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'version' with id #666 has not been found.", 'code': 1}, resp.json()) @@ -44,32 +44,32 @@ def test_create_game_version_not_found(self): def test_create_update_delete_success(self): # Create payload = {'magazineIssueId': 1, 'gameVersionId': 1, 'type': 'Preview', 'pageNumber': 49, 'notes': 'A preview mention.'} - resp = self.api_call('post', 'game-version-magazine-mention', payload, True) + resp = self.api_call('post', 'game-version-magazine-mentions', payload, True) self.assertEqual(200, resp.status_code) self.assertEqual('Preview', resp.json()['type']) mention_id = str(resp.json()['id']) - resp = self.api_call('get', 'game-version-magazine-mention/' + mention_id, None, True) + resp = self.api_call('get', 'game-version-magazine-mentions/' + mention_id, None, True) payload['id'] = 4 self.assertEqual(payload, resp.json()) # Patch - resp = self.api_call('patch', 'game-version-magazine-mention/' + mention_id, {'notes': 'Updated.'}, True) + resp = self.api_call('patch', 'game-version-magazine-mentions/' + mention_id, {'notes': 'Updated.'}, True) self.assertEqual(200, resp.status_code) self.assertEqual('Updated.', resp.json()['notes']) # Delete - resp = self.api_call('delete', 'game-version-magazine-mention/' + mention_id, {}, True) + resp = self.api_call('delete', 'game-version-magazine-mentions/' + mention_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'game-version-magazine-mention/' + mention_id, {}, True) + resp = self.api_call('delete', 'game-version-magazine-mentions/' + mention_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': f"The resource of type 'game_version_magazine_mention' with id #{mention_id} has not been found.", 'code': 1}, resp.json()) def test_update_not_found(self): - resp = self.api_call('patch', 'game-version-magazine-mention/666', {'notes': 'Whatever'}, True) + resp = self.api_call('patch', 'game-version-magazine-mentions/666', {'notes': 'Whatever'}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'game_version_magazine_mention' with id #666 has not been found.", 'code': 1}, resp.json()) diff --git a/test/functional/test_games.py b/test/functional/test_games.py index efdf193..3d60032 100644 --- a/test/functional/test_games.py +++ b/test/functional/test_games.py @@ -2,30 +2,30 @@ class TestPlatforms(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('game') - super().check_all_routes_error_missing_user_token('game') + super().check_all_routes_error_bad_user_token('games') + super().check_all_routes_error_missing_user_token('games') def test_get_game(self): # Does not exist - resp = self.api_call('get', 'game/666', {}, True) + resp = self.api_call('get', 'games/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'game' with id #666 has not been found.", 'code': 1}, resp.json()) # Exist - resp = self.api_call('get', 'game/1', {}, True) + resp = self.api_call('get', 'games/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'id': 1, 'title': 'Sega Soccer', 'notes': 'super jeu !!!', 'versionCount': 1}, resp.json()) def test_create_incomplete_payload(self): - resp = self.api_call('post', 'game', {}, True) + resp = self.api_call('post', 'games', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: title.', 'code': 6}, resp.json()) def test_create_duplicate_title(self): - resp = self.api_call('post', 'game', {'title': 'Fifa 97', 'finished': False}, True) + resp = self.api_call('post', 'games', {'title': 'Fifa 97', 'finished': False}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'game' with title 'Fifa 97' already exists.", 'code': 8}, resp.json()) @@ -33,45 +33,45 @@ def test_create_duplicate_title(self): def test_create_update_delete_success(self): # Create payload = {'title': 'Something', 'notes': 'First played at it in 1997.'} - resp = self.api_call('post', 'game', payload, True) + resp = self.api_call('post', 'games', payload, True) self.assertEqual(200, resp.status_code) self.assertEqual('Something', resp.json()["title"]) game_id = str(resp.json()["id"]) - resp = self.api_call('get', 'game/' + game_id, None, True) + resp = self.api_call('get', 'games/' + game_id, None, True) payload['id'] = 381 payload['versionCount'] = 0 self.assertEqual(payload, resp.json()) # Patch new_title = 'Something II - ' + game_id - resp = self.api_call('patch', 'game/' + game_id, {'title': new_title}, True) + resp = self.api_call('patch', 'games/' + game_id, {'title': new_title}, True) self.assertEqual(200, resp.status_code) self.assertEqual(new_title, resp.json()["title"]) # Delete - resp = self.api_call('delete', 'game/' + game_id, {}, True) + resp = self.api_call('delete', 'games/' + game_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'game/' + game_id, {}, True) + resp = self.api_call('delete', 'games/' + game_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': f"The resource of type 'game' with id #{game_id} has not been found.", 'code': 1}, resp.json()) def test_update_duplicate_title(self): - resp = self.api_call('patch', 'game/4', {'title': 'Fifa 97'}, True) + resp = self.api_call('patch', 'games/4', {'title': 'Fifa 97'}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'game' with title 'Revenge Of Shinobi' already exists.", 'code': 8}, resp.json()) def test_delete_fails_because_game_has_versions(self): - resp = self.api_call('delete', 'game/1', {}, True) + resp = self.api_call('delete', 'games/1', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The following resource type 'game' has children of type 'version', so it cannot be deleted.", 'code': 9}, resp.json()) - resp = self.api_call('get', 'game/1', {}, True) + resp = self.api_call('get', 'games/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'id': 1, 'title': 'Sega Soccer', 'notes': 'super jeu !!!', 'versionCount': 1}, resp.json()) diff --git a/test/functional/test_magazine_issue_copies.py b/test/functional/test_magazine_issue_copies.py index 87c728e..068a59d 100644 --- a/test/functional/test_magazine_issue_copies.py +++ b/test/functional/test_magazine_issue_copies.py @@ -2,35 +2,35 @@ class TestMagazineIssueCopies(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('magazine-issue-copy') - super().check_all_routes_error_missing_user_token('magazine-issue-copy') + super().check_all_routes_error_bad_user_token('magazine-issue-copies') + super().check_all_routes_error_missing_user_token('magazine-issue-copies') def test_get_magazine_issue_copy(self): # Does not exist - resp = self.api_call('get', 'magazine-issue-copy/666', {}, True) + resp = self.api_call('get', 'magazine-issue-copies/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue_copy' with id #666 has not been found.", 'code': 1}, resp.json()) # Exists - resp = self.api_call('get', 'magazine-issue-copy/1', {}, True) + resp = self.api_call('get', 'magazine-issue-copies/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'id': 1, 'magazineIssueId': 1, 'type': 'Printed-Original', 'notes': 'Original'}, resp.json()) def test_create_incomplete_payload(self): - resp = self.api_call('post', 'magazine-issue-copy', {}, True) + resp = self.api_call('post', 'magazine-issue-copies', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: magazineIssueId.', 'code': 6}, resp.json()) def test_create_unsupported_type(self): - resp = self.api_call('post', 'magazine-issue-copy', {'magazineIssueId': 1, 'type': 'Vinyl'}, True) + resp = self.api_call('post', 'magazine-issue-copies', {'magazineIssueId': 1, 'type': 'Vinyl'}, True) self.assertEqual(400, resp.status_code) def test_create_magazine_issue_not_found(self): - resp = self.api_call('post', 'magazine-issue-copy', {'magazineIssueId': 666, 'type': 'Printed-Original'}, True) + resp = self.api_call('post', 'magazine-issue-copies', {'magazineIssueId': 666, 'type': 'Printed-Original'}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) @@ -38,32 +38,32 @@ def test_create_magazine_issue_not_found(self): def test_create_update_delete_success(self): # Create payload = {'magazineIssueId': 1, 'type': 'Digital', 'notes': 'Second digital copy.'} - resp = self.api_call('post', 'magazine-issue-copy', payload, True) + resp = self.api_call('post', 'magazine-issue-copies', payload, True) self.assertEqual(200, resp.status_code) self.assertEqual('Digital', resp.json()['type']) copy_id = str(resp.json()['id']) - resp = self.api_call('get', 'magazine-issue-copy/' + copy_id, None, True) + resp = self.api_call('get', 'magazine-issue-copies/' + copy_id, None, True) payload['id'] = 3 self.assertEqual(payload, resp.json()) # Patch - resp = self.api_call('patch', 'magazine-issue-copy/' + copy_id, {'notes': 'Updated note.'}, True) + resp = self.api_call('patch', 'magazine-issue-copies/' + copy_id, {'notes': 'Updated note.'}, True) self.assertEqual(200, resp.status_code) self.assertEqual('Updated note.', resp.json()['notes']) # Delete - resp = self.api_call('delete', 'magazine-issue-copy/' + copy_id, {}, True) + resp = self.api_call('delete', 'magazine-issue-copies/' + copy_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'magazine-issue-copy/' + copy_id, {}, True) + resp = self.api_call('delete', 'magazine-issue-copies/' + copy_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': f"The resource of type 'magazine_issue_copy' with id #{copy_id} has not been found.", 'code': 1}, resp.json()) def test_update_not_found(self): - resp = self.api_call('patch', 'magazine-issue-copy/666', {'notes': 'Whatever'}, True) + resp = self.api_call('patch', 'magazine-issue-copies/666', {'notes': 'Whatever'}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue_copy' with id #666 has not been found.", 'code': 1}, resp.json()) diff --git a/test/functional/test_magazine_issues.py b/test/functional/test_magazine_issues.py index c9837ee..19b40d4 100644 --- a/test/functional/test_magazine_issues.py +++ b/test/functional/test_magazine_issues.py @@ -2,36 +2,36 @@ class TestMagazineIssues(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('magazine-issue') - super().check_all_routes_error_missing_user_token('magazine-issue') + super().check_all_routes_error_bad_user_token('magazine-issues') + super().check_all_routes_error_missing_user_token('magazine-issues') def test_get_magazine_issue(self): # Does not exist - resp = self.api_call('get', 'magazine-issue/666', {}, True) + resp = self.api_call('get', 'magazine-issues/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) # Exists - resp = self.api_call('get', 'magazine-issue/1', {}, True) + resp = self.api_call('get', 'magazine-issues/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'id': 1, 'magazineId': 1, 'issueNumber': 3, 'year': 1997, 'month': 10, 'notes': 'Le troisième !'}, resp.json()) def test_create_incomplete_payload(self): - resp = self.api_call('post', 'magazine-issue', {}, True) + resp = self.api_call('post', 'magazine-issues', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: magazineId.', 'code': 6}, resp.json()) def test_create_magazine_not_found(self): - resp = self.api_call('post', 'magazine-issue', {'magazineId': 666, 'issueNumber': 1, 'year': 2000, 'month': 1}, True) + resp = self.api_call('post', 'magazine-issues', {'magazineId': 666, 'issueNumber': 1, 'year': 2000, 'month': 1}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine' with id #666 has not been found.", 'code': 1}, resp.json()) def test_create_duplicate_issue_number(self): - resp = self.api_call('post', 'magazine-issue', {'magazineId': 1, 'issueNumber': 1, 'year': 1997, 'month': 8}, True) + resp = self.api_call('post', 'magazine-issues', {'magazineId': 1, 'issueNumber': 1, 'year': 1997, 'month': 8}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue' with issue_number '1' already exists.", 'code': 8}, resp.json()) @@ -39,58 +39,58 @@ def test_create_duplicate_issue_number(self): def test_create_update_delete_success(self): # Create payload = {'magazineId': 1, 'issueNumber': 4, 'year': 1998, 'month': 1, 'notes': 'Le quatrième.'} - resp = self.api_call('post', 'magazine-issue', payload, True) + resp = self.api_call('post', 'magazine-issues', payload, True) self.assertEqual(200, resp.status_code) self.assertEqual(4, resp.json()['issueNumber']) issue_id = str(resp.json()['id']) - resp = self.api_call('get', 'magazine-issue/' + issue_id, None, True) + resp = self.api_call('get', 'magazine-issues/' + issue_id, None, True) payload['id'] = 4 self.assertEqual(payload, resp.json()) # Patch - resp = self.api_call('patch', 'magazine-issue/' + issue_id, {'notes': 'Mis à jour.'}, True) + resp = self.api_call('patch', 'magazine-issues/' + issue_id, {'notes': 'Mis à jour.'}, True) self.assertEqual(200, resp.status_code) self.assertEqual('Mis à jour.', resp.json()['notes']) # Delete - resp = self.api_call('delete', 'magazine-issue/' + issue_id, {}, True) + resp = self.api_call('delete', 'magazine-issues/' + issue_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'magazine-issue/' + issue_id, {}, True) + resp = self.api_call('delete', 'magazine-issues/' + issue_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': f"The resource of type 'magazine_issue' with id #{issue_id} has not been found.", 'code': 1}, resp.json()) def test_update_not_found(self): - resp = self.api_call('patch', 'magazine-issue/666', {'notes': 'Whatever'}, True) + resp = self.api_call('patch', 'magazine-issues/666', {'notes': 'Whatever'}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue' with id #666 has not been found.", 'code': 1}, resp.json()) def test_update_duplicate_issue_number(self): - resp = self.api_call('patch', 'magazine-issue/3', {'issueNumber': 1}, True) + resp = self.api_call('patch', 'magazine-issues/3', {'issueNumber': 1}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine_issue' with issue_number '1' already exists.", 'code': 8}, resp.json()) def test_delete_fails_because_issue_has_copies(self): - resp = self.api_call('delete', 'magazine-issue/1', {}, True) + resp = self.api_call('delete', 'magazine-issues/1', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The following resource type 'magazine_issue' has children of type 'copy', so it cannot be deleted.", 'code': 9}, resp.json()) - resp = self.api_call('get', 'magazine-issue/1', {}, True) + resp = self.api_call('get', 'magazine-issues/1', {}, True) self.assertEqual(200, resp.status_code) def test_delete_fails_because_issue_has_mentions(self): - resp = self.api_call('delete', 'magazine-issue/2', {}, True) + resp = self.api_call('delete', 'magazine-issues/2', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The following resource type 'magazine_issue' has children of type 'game_version_magazine_mention', so it cannot be deleted.", 'code': 9}, resp.json()) - resp = self.api_call('get', 'magazine-issue/2', {}, True) + resp = self.api_call('get', 'magazine-issues/2', {}, True) self.assertEqual(200, resp.status_code) def test_get_list_default_filters(self): diff --git a/test/functional/test_magazines.py b/test/functional/test_magazines.py index a156224..04a1734 100644 --- a/test/functional/test_magazines.py +++ b/test/functional/test_magazines.py @@ -2,30 +2,30 @@ class TestMagazines(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('magazine') - super().check_all_routes_error_missing_user_token('magazine') + super().check_all_routes_error_bad_user_token('magazines') + super().check_all_routes_error_missing_user_token('magazines') def test_get_magazine(self): # Does not exist - resp = self.api_call('get', 'magazine/666', {}, True) + resp = self.api_call('get', 'magazines/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine' with id #666 has not been found.", 'code': 1}, resp.json()) # Exists - resp = self.api_call('get', 'magazine/1', {}, True) + resp = self.api_call('get', 'magazines/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'id': 1, 'title': 'Gen4', 'notes': 'Découvert en 1997.', 'issueCount': 3}, resp.json()) def test_create_incomplete_payload(self): - resp = self.api_call('post', 'magazine', {}, True) + resp = self.api_call('post', 'magazines', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: title.', 'code': 6}, resp.json()) def test_create_duplicate_title(self): - resp = self.api_call('post', 'magazine', {'title': 'Gen4'}, True) + resp = self.api_call('post', 'magazines', {'title': 'Gen4'}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine' with title 'Gen4' already exists.", 'code': 8}, resp.json()) @@ -33,51 +33,51 @@ def test_create_duplicate_title(self): def test_create_update_delete_success(self): # Create payload = {'title': 'Joystick', 'notes': 'French gaming magazine.'} - resp = self.api_call('post', 'magazine', payload, True) + resp = self.api_call('post', 'magazines', payload, True) self.assertEqual(200, resp.status_code) self.assertEqual('Joystick', resp.json()['title']) magazine_id = str(resp.json()['id']) - resp = self.api_call('get', 'magazine/' + magazine_id, None, True) + resp = self.api_call('get', 'magazines/' + magazine_id, None, True) payload['id'] = 3 payload['issueCount'] = 0 # newly created, no issues yet self.assertEqual(payload, resp.json()) # Patch new_title = 'Joystick II - ' + magazine_id - resp = self.api_call('patch', 'magazine/' + magazine_id, {'title': new_title}, True) + resp = self.api_call('patch', 'magazines/' + magazine_id, {'title': new_title}, True) self.assertEqual(200, resp.status_code) self.assertEqual(new_title, resp.json()['title']) # Delete - resp = self.api_call('delete', 'magazine/' + magazine_id, {}, True) + resp = self.api_call('delete', 'magazines/' + magazine_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'magazine/' + magazine_id, {}, True) + resp = self.api_call('delete', 'magazines/' + magazine_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': f"The resource of type 'magazine' with id #{magazine_id} has not been found.", 'code': 1}, resp.json()) def test_update_not_found(self): - resp = self.api_call('patch', 'magazine/666', {'title': 'Whatever'}, True) + resp = self.api_call('patch', 'magazines/666', {'title': 'Whatever'}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine' with id #666 has not been found.", 'code': 1}, resp.json()) def test_update_duplicate_title(self): - resp = self.api_call('patch', 'magazine/2', {'title': 'Gen4'}, True) + resp = self.api_call('patch', 'magazines/2', {'title': 'Gen4'}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'magazine' with title 'Gen4' already exists.", 'code': 8}, resp.json()) def test_delete_fails_because_magazine_has_issues(self): - resp = self.api_call('delete', 'magazine/1', {}, True) + resp = self.api_call('delete', 'magazines/1', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The following resource type 'magazine' has children of type 'issue', so it cannot be deleted.", 'code': 9}, resp.json()) - resp = self.api_call('get', 'magazine/1', {}, True) + resp = self.api_call('get', 'magazines/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual('Gen4', resp.json()['title']) diff --git a/test/functional/test_notes.py b/test/functional/test_notes.py index cf8e699..f8d2fd2 100644 --- a/test/functional/test_notes.py +++ b/test/functional/test_notes.py @@ -2,31 +2,31 @@ class TestNotes(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('note') - super().check_all_routes_error_missing_user_token('note') + super().check_all_routes_error_bad_user_token('notes') + super().check_all_routes_error_missing_user_token('notes') def test_get_note(self): # Does not exist - resp = self.api_call('get', 'note/666', {}, True) + resp = self.api_call('get', 'notes/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'note' with id #666 has not been found.", 'code': 1}, resp.json()) # Exist - resp = self.api_call('get', 'note/1', {}, True) + resp = self.api_call('get', 'notes/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'id': 1, 'title': 'Note 1', 'content': 'Some comment 1.', 'gameVersionId': None}, resp.json()) def test_create_incomplete_payload(self): - resp = self.api_call('post', 'note', {}, True) + resp = self.api_call('post', 'notes', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: title.', 'code': 6}, resp.json()) def test_create_update_delete_success(self): # Create - resp = self.api_call('post', 'note', {'title': 'Genesis', 'content': 'Pues'}, True) + resp = self.api_call('post', 'notes', {'title': 'Genesis', 'content': 'Pues'}, True) self.assertEqual(200, resp.status_code) self.assertEqual('Genesis', resp.json()["title"]) @@ -34,16 +34,16 @@ def test_create_update_delete_success(self): note_id = str(resp.json()["id"]) # Patch - resp = self.api_call('patch', 'note/' + note_id, {'title': 'Playstation III'}, True) + resp = self.api_call('patch', 'notes/' + note_id, {'title': 'Playstation III'}, True) self.assertEqual(200, resp.status_code) self.assertEqual('Playstation III', resp.json()["title"]) # Delete - resp = self.api_call('delete', 'note/' + note_id, {}, True) + resp = self.api_call('delete', 'notes/' + note_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'note/' + note_id, {}, True) + resp = self.api_call('delete', 'notes/' + note_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'note' with id #3 has not been found.", 'code': 1}, resp.json()) diff --git a/test/functional/test_platforms.py b/test/functional/test_platforms.py index c568669..b412998 100644 --- a/test/functional/test_platforms.py +++ b/test/functional/test_platforms.py @@ -2,37 +2,37 @@ class TestPlatforms(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('platform') - super().check_all_routes_error_missing_user_token('platform') + super().check_all_routes_error_bad_user_token('platforms') + super().check_all_routes_error_missing_user_token('platforms') def test_get_platform(self): # Does not exist - resp = self.api_call('get', 'platform/666', {}, True) + resp = self.api_call('get', 'platforms/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'platform' with id #666 has not been found.", 'code': 1}, resp.json()) # Exist - resp = self.api_call('get', 'platform/1', {}, True) + resp = self.api_call('get', 'platforms/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'id': 1, 'name': 'PC', 'versionCount': 232}, resp.json()) def test_create_incomplete_payload(self): - resp = self.api_call('post', 'platform', {}, True) + resp = self.api_call('post', 'platforms', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: name.', 'code': 6}, resp.json()) def test_create_duplicate_name(self): - resp = self.api_call('post', 'platform', {'name': 'PC'}, True) + resp = self.api_call('post', 'platforms', {'name': 'PC'}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'platform' with name 'PC' already exists.", 'code': 8}, resp.json()) def test_create_update_delete_success(self): # Create - resp = self.api_call('post', 'platform', {'name': 'Genesis'}, True) + resp = self.api_call('post', 'platforms', {'name': 'Genesis'}, True) self.assertEqual(200, resp.status_code) self.assertEqual('Genesis', resp.json()["name"]) @@ -40,32 +40,32 @@ def test_create_update_delete_success(self): platform_id = str(resp.json()["id"]) # Patch - resp = self.api_call('patch', 'platform/' + platform_id, {'name': 'Playstation III'}, True) + resp = self.api_call('patch', 'platforms/' + platform_id, {'name': 'Playstation III'}, True) self.assertEqual(200, resp.status_code) self.assertEqual('Playstation III', resp.json()["name"]) # Delete - resp = self.api_call('delete', 'platform/' + platform_id, {}, True) + resp = self.api_call('delete', 'platforms/' + platform_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'platform/' + platform_id, {}, True) + resp = self.api_call('delete', 'platforms/' + platform_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'platform' with id #12 has not been found.", 'code': 1}, resp.json()) def test_update_duplicate_name(self): - resp = self.api_call('patch', 'platform/4', {'name': 'PC'}, True) + resp = self.api_call('patch', 'platforms/4', {'name': 'PC'}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'platform' with name 'Nintendo 64' already exists.", 'code': 8}, resp.json()) def test_delete_fails_because_platform_has_versions(self): - resp = self.api_call('delete', 'platform/1', {}, True) + resp = self.api_call('delete', 'platforms/1', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The following resource type 'platform' has children of type 'version', so it cannot be deleted.", 'code': 9}, resp.json()) - resp = self.api_call('get', 'platform/1', {}, True) + resp = self.api_call('get', 'platforms/1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'id': 1, 'name': 'PC', 'versionCount': 232}, resp.json()) diff --git a/test/functional/test_stories.py b/test/functional/test_stories.py index 29936a7..fd97951 100644 --- a/test/functional/test_stories.py +++ b/test/functional/test_stories.py @@ -2,8 +2,8 @@ class TestCopies(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('story') - super().check_all_routes_error_missing_user_token('story') + super().check_all_routes_error_bad_user_token('stories') + super().check_all_routes_error_missing_user_token('stories') def test_create_incomplete_payload(self): payload = { @@ -12,7 +12,7 @@ def test_create_incomplete_payload(self): "watched": True, "played": False } - resp = self.api_call('post', 'story', payload, True) + resp = self.api_call('post', 'stories', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: versionId.', 'code': 6}, resp.json()) @@ -25,20 +25,20 @@ def test_create_fails_version_not_found(self): "watched": True, "played": False } - resp = self.api_call('post', 'story', payload, True) + resp = self.api_call('post', 'stories', payload, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'version' with id #9999 has not been found.", 'code': 1}, resp.json()) def test_get_story(self): # Does not exist - resp = self.api_call('get', 'story/666', {}, True) + resp = self.api_call('get', 'stories/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'story' with id #666 has not been found.", 'code': 1}, resp.json()) # Exist - resp = self.api_call('get', 'story/2', {}, True) + resp = self.api_call('get', 'stories/2', {}, True) expectedPayload = { "id": 2, @@ -64,7 +64,7 @@ def test_create_update_delete_success(self): "played": False } - resp = self.api_call('post', 'story', payload, True) + resp = self.api_call('post', 'stories', payload, True) self.assertEqual(200, resp.status_code) story_id = str(resp.json()["id"]) @@ -73,7 +73,7 @@ def test_create_update_delete_success(self): payload['gameTitle'] = resp.json()["gameTitle"] self.assertEqual(payload, resp.json()) - resp = self.api_call('get', 'story/' + str(story_id), None, True) + resp = self.api_call('get', 'stories/' + str(story_id), None, True) self.assertEqual(payload, resp.json()) # Patch @@ -85,7 +85,7 @@ def test_create_update_delete_success(self): "played": False } - resp = self.api_call('patch', 'story/' + story_id, payload, True) + resp = self.api_call('patch', 'stories/' + story_id, payload, True) payload['id'] = int(story_id) payload['platformName'] = resp.json()["platformName"] payload['gameTitle'] = resp.json()["gameTitle"] @@ -94,21 +94,21 @@ def test_create_update_delete_success(self): self.assertEqual(payload, resp.json()) # Delete - resp = self.api_call('delete', 'story/' + story_id, {}, True) + resp = self.api_call('delete', 'stories/' + story_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'story/' + story_id, {}, True) + resp = self.api_call('delete', 'stories/' + story_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': f"The resource of type 'story' with id #{story_id} has not been found.", 'code': 1}, resp.json()) def test_update_fails_because_resource_not_found(self): - resp = self.api_call('patch', 'story/9999', None, True) + resp = self.api_call('patch', 'stories/9999', None, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'story' with id #9999 has not been found.", 'code': 1}, resp.json()) def test_delete_fails_because_not_found(self): - resp = self.api_call('delete', 'story/9999', None, True) + resp = self.api_call('delete', 'stories/9999', None, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'story' with id #9999 has not been found.", 'code': 1}, resp.json()) diff --git a/test/functional/test_transactions.py b/test/functional/test_transactions.py index 81d6fac..a127375 100644 --- a/test/functional/test_transactions.py +++ b/test/functional/test_transactions.py @@ -2,8 +2,8 @@ class TestTransactions(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('transaction') - super().check_all_routes_error_missing_user_token('transaction') + super().check_all_routes_error_bad_user_token('transactions') + super().check_all_routes_error_missing_user_token('transactions') def test_create_incomplete_payload(self): payload = { @@ -13,7 +13,7 @@ def test_create_incomplete_payload(self): "type": "Loan-out", "notes": "" } - resp = self.api_call('post', 'transaction', payload, True) + resp = self.api_call('post', 'transactions', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: versionId.', 'code': 6}, resp.json()) @@ -28,7 +28,7 @@ def test_create_fails_version_id_not_found(self): "type": "Loan-out", "notes": "" } - resp = self.api_call('post', 'transaction', payload, True) + resp = self.api_call('post', 'transactions', payload, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'version' with id #99998 has not been found.", 'code': 1}, resp.json()) @@ -44,7 +44,7 @@ def test_create_fails_copy_id_not_found(self): "type": "Loan-out", "notes": "" } - resp = self.api_call('post', 'transaction', payload, True) + resp = self.api_call('post', 'transactions', payload, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'copy' with id #99999 has not been found.", 'code': 1}, resp.json()) @@ -60,20 +60,20 @@ def test_create_invalid_types(self): "notes": "" } - resp = self.api_call('post', 'transaction', payload, True) + resp = self.api_call('post', 'transactions', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The field 'type' does not support the value 'Loan-oute'. Supported values are: Bought, Loan-in, Loan-in-return, Loan-out, Loan-out-return, Sold.", 'code': 11}, resp.json()) def test_get_transaction(self): # Does not exist - resp = self.api_call('get', 'transaction/666', {}, True) + resp = self.api_call('get', 'transactions/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'transaction' with id #666 has not been found.", 'code': 1}, resp.json()) # Exist - resp = self.api_call('get', 'transaction/90', {}, True) + resp = self.api_call('get', 'transactions/90', {}, True) expectedPayload = { "id": 90, @@ -103,7 +103,7 @@ def test_create_update_delete_success(self): "notes": "" } - resp = self.api_call('post', 'transaction', payload, True) + resp = self.api_call('post', 'transactions', payload, True) self.assertEqual(200, resp.status_code) transaction_id = str(resp.json()["id"]) @@ -112,7 +112,7 @@ def test_create_update_delete_success(self): payload['gameTitle'] = resp.json()["gameTitle"] self.assertEqual(payload, resp.json()) - resp = self.api_call('get', 'transaction/' + str(transaction_id), None, True) + resp = self.api_call('get', 'transactions/' + str(transaction_id), None, True) self.assertEqual(payload, resp.json()) # Patch @@ -125,7 +125,7 @@ def test_create_update_delete_success(self): "notes": "" } - resp = self.api_call('patch', 'transaction/' + transaction_id, payload, True) + resp = self.api_call('patch', 'transactions/' + transaction_id, payload, True) payload['id'] = int(transaction_id) payload['platformName'] = resp.json()["platformName"] payload['gameTitle'] = resp.json()["gameTitle"] @@ -135,15 +135,15 @@ def test_create_update_delete_success(self): self.assertEqual(payload, resp.json()) # Delete - resp = self.api_call('delete', 'transaction/' + transaction_id, {}, True) + resp = self.api_call('delete', 'transactions/' + transaction_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'transaction/' + transaction_id, {}, True) + resp = self.api_call('delete', 'transactions/' + transaction_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': f"The resource of type 'transaction' with id #{transaction_id} has not been found.", 'code': 1}, resp.json()) def test_update_fails_because_transaction_not_found(self): - resp = self.api_call('patch', 'transaction/9999', None, True) + resp = self.api_call('patch', 'transactions/9999', None, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'transaction' with id #9999 has not been found.", 'code': 1}, resp.json()) @@ -158,7 +158,7 @@ def test_create_fails_because_version_id_and_copy_version_id_dont_match(self): "type": "Loan-out", "notes": "" } - resp = self.api_call('post', 'transaction', payload, True) + resp = self.api_call('post', 'transactions', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "Inconsistent transaction. You tried to create a transaction with versionId = '349' while the copy versionId is '348'.", 'code': 14}, resp.json()) @@ -173,7 +173,7 @@ def test_update_fails_because_version_id_and_copy_version_id_dont_match(self): "type": "Loan-out", "notes": "" } - resp = self.api_call('patch', 'transaction/90', payload, True) + resp = self.api_call('patch', 'transactions/90', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "Inconsistent transaction. You tried to create a transaction with versionId = '349' while the copy versionId is '348'.", 'code': 14}, resp.json()) @@ -189,13 +189,13 @@ def test_update_fails_invalid_types(self): "notes": "" } - resp = self.api_call('patch', 'transaction/90', payload, True) + resp = self.api_call('patch', 'transactions/90', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The field 'type' does not support the value 'Loan-ine'. Supported values are: Bought, Loan-in, Loan-in-return, Loan-out, Loan-out-return, Sold.", 'code': 11}, resp.json()) def test_delete_fails_because_not_found(self): - resp = self.api_call('delete', 'transaction/9999', None, True) + resp = self.api_call('delete', 'transactions/9999', None, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'transaction' with id #9999 has not been found.", 'code': 1}, resp.json()) @@ -252,7 +252,7 @@ def test_copy_status_toggle(self): "comments": "Well well well..." } - resp = self.api_call('post', 'copy', payload, True) + resp = self.api_call('post', 'copies', payload, True) copy_id = str(resp.json()["id"]) payload['id'] = int(copy_id) @@ -271,7 +271,7 @@ def test_copy_status_toggle(self): 'platformName': 'PC', } - resp = self.api_call('post', 'transaction', tr_payload, True) + resp = self.api_call('post', 'transactions', tr_payload, True) self.assertEqual(200, resp.status_code) tr_id = str(resp.json()["id"]) @@ -279,15 +279,15 @@ def test_copy_status_toggle(self): self.assertEqual(tr_payload, resp.json()) # Now, the current copy status should be "Out" - resp = self.api_call('get', 'copy/' + copy_id, {}, True) + resp = self.api_call('get', 'copies/' + copy_id, {}, True) self.assertEqual('Out', resp.json()['status']) # If we DELETE the transaction (let's say it was a mistake to create it...) # The status of the copy must remain at "Out" - resp = self.api_call('delete', 'transaction/' + tr_id, {}, True) + resp = self.api_call('delete', 'transactions/' + tr_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('get', 'copy/' + copy_id, {}, True) + resp = self.api_call('get', 'copies/' + copy_id, {}, True) self.assertEqual('Out', resp.json()['status']) # Now let's create an inbound transaction @@ -301,13 +301,13 @@ def test_copy_status_toggle(self): "notes": "" } - resp = self.api_call('post', 'transaction', tr_payload, True) + resp = self.api_call('post', 'transactions', tr_payload, True) self.assertEqual(200, resp.status_code) tr_id = str(resp.json()["id"]) # Now, the current copy status should be "In" - resp = self.api_call('get', 'copy/' + copy_id, {}, True) + resp = self.api_call('get', 'copies/' + copy_id, {}, True) self.assertEqual('In', resp.json()['status']) # Now let's change the transaction type to 'Sold' @@ -317,15 +317,15 @@ def test_copy_status_toggle(self): "type": "Sold", } - resp = self.api_call('patch', 'transaction/' + tr_id, tr_payload, True) + resp = self.api_call('patch', 'transactions/' + tr_id, tr_payload, True) self.assertEqual(200, resp.status_code) self.assertEqual(None, resp.json()["copyId"]) - resp = self.api_call('get', 'copy/' + copy_id, {}, True) + resp = self.api_call('get', 'copies/' + copy_id, {}, True) self.assertEqual(404, resp.status_code) # Delete for cleanup - resp = self.api_call('delete', 'transaction/' + tr_id, {}, True) + resp = self.api_call('delete', 'transactions/' + tr_id, {}, True) self.assertEqual(200, resp.status_code) def test_inconsistent_status_operation(self): @@ -347,7 +347,7 @@ def test_inconsistent_status_operation(self): "comments": "Well well well..." } - resp = self.api_call('post', 'copy', payload, True) + resp = self.api_call('post', 'copies', payload, True) copy_id = str(resp.json()["id"]) payload['id'] = int(copy_id) @@ -364,17 +364,17 @@ def test_inconsistent_status_operation(self): "notes": "" } - resp = self.api_call('post', 'transaction', tr_payload, True) + resp = self.api_call('post', 'transactions', tr_payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'code': 4, 'message': "Inconsistent transaction. You tried to create a transaction of type 'Loan-out' while the copy status is 'Out'."}, resp.json()) # The current copy status should still be "Out" - resp = self.api_call('get', 'copy/' + copy_id, {}, True) + resp = self.api_call('get', 'copies/' + copy_id, {}, True) self.assertEqual('Out', resp.json()['status']) # Delete for cleanup - resp = self.api_call('delete', 'copy/' + copy_id, {}, True) + resp = self.api_call('delete', 'copies/' + copy_id, {}, True) self.assertEqual(200, resp.status_code) def test_can_create_with_no_copy_id(self): @@ -390,7 +390,7 @@ def test_can_create_with_no_copy_id(self): 'platformName': 'PC', } - resp = self.api_call('post', 'transaction', tr_payload, True) + resp = self.api_call('post', 'transactions', tr_payload, True) self.assertEqual(200, resp.status_code) tr_id = str(resp.json()["id"]) @@ -398,7 +398,7 @@ def test_can_create_with_no_copy_id(self): self.assertEqual(tr_payload, resp.json()) # Delete for cleanup - resp = self.api_call('delete', 'transaction/' + tr_id, {}, True) + resp = self.api_call('delete', 'transactions/' + tr_id, {}, True) self.assertEqual(200, resp.status_code) @@ -429,7 +429,7 @@ def test_cannot_create_two_inbound_transaction_in_a_row(self): "comments": "Well well well..." } - resp = self.api_call('post', 'copy', payload, True) + resp = self.api_call('post', 'copies', payload, True) copy_id = str(resp.json()["id"]) # Now let's create the first inbound transaction @@ -445,7 +445,7 @@ def test_cannot_create_two_inbound_transaction_in_a_row(self): 'platformName': 'PC', } - resp = self.api_call('post', 'transaction', tr_payload, True) + resp = self.api_call('post', 'transactions', tr_payload, True) self.assertEqual(200, resp.status_code) tr_id = str(resp.json()["id"]) @@ -453,13 +453,13 @@ def test_cannot_create_two_inbound_transaction_in_a_row(self): self.assertEqual(tr_payload, resp.json()) # Now, let's try the same operation: it should fail - resp = self.api_call('post', 'transaction', tr_payload, True) + resp = self.api_call('post', 'transactions', tr_payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'code': 15, 'message': "Inconsistent transaction. You tried to create a transaction an inbound transaction while the last registered transaction for this copy has of the same kind!"}, resp.json()) # Delete for cleanup - resp = self.api_call('delete', 'transaction/' + tr_id, {}, True) + resp = self.api_call('delete', 'transactions/' + tr_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'copy/' + copy_id, {}, True) + resp = self.api_call('delete', 'copies/' + copy_id, {}, True) self.assertEqual(200, resp.status_code) diff --git a/test/functional/test_users.py b/test/functional/test_users.py index 828f797..03f5521 100644 --- a/test/functional/test_users.py +++ b/test/functional/test_users.py @@ -2,48 +2,48 @@ class TestUsers(AbstractTestsTools): def test_basic_authentication_missing_header(self): - resp = self.api_call('post', 'user/authenticate') + resp = self.api_call('post', 'users/authenticate') self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following header is missing: Authorization.', 'code': 7}, resp.json()) def test_authentication_user_not_found(self): headers = {'Authorization': 'Basic YWxhZGRpbjpzZXNhbWVPdXZyZVRvaQ=='} - resp = self.api_call('post', 'user/authenticate', None, False, headers) + resp = self.api_call('post', 'users/authenticate', None, False, headers) self.assertEqual(403, resp.status_code) self.assertEqual({'code': 1, 'message': "The resource of type 'user' with username 'aladdin' has not been found."}, resp.json()) def test_authentication_impossible_to_decode_header(self): headers = {'Authorization': 'Basic Am0v1mJhcg=='} - resp = self.api_call('post', 'user/authenticate', None, False, headers) + resp = self.api_call('post', 'users/authenticate', None, False, headers) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'Impossible to decode the value of the authentication header.', 'code': 5}, resp.json()) def test_creation_missing_auth_header(self): payload = {'email': 'foo', 'password': 'bar', 'username': 'someusername'} - resp = self.api_call('post', 'user', payload) + resp = self.api_call('post', 'users', payload) self.assertEqual(403, resp.status_code) self.assertEqual({'message': 'Missing token', 'code': 12}, resp.json()) def test_creation_incomplete_payload(self): payload = {'email': 'foo', 'password': 'bar'} - resp = self.api_call('post', 'user', payload, True) + resp = self.api_call('post', 'users', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: username.', 'code': 6}, resp.json()) def test_update_fails_user_not_found(self): - resp = self.api_call('patch', 'user/666', {}, True) + resp = self.api_call('patch', 'users/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'user' with id #666 has not been found.", 'code': 1}, resp.json()) def test_creation_update_activate_renew_token(self): # Create the user payload = {'email': 'foo', 'password': 'bar', 'username': 'mephistopheles'} - resp = self.api_call('post', 'user', payload, True) + resp = self.api_call('post', 'users', payload, True) self.assertEqual(200, resp.status_code) self.assertEqual(False, resp.json()["active"]) @@ -53,21 +53,21 @@ def test_creation_update_activate_renew_token(self): # Try to authenticate as the new user: fails because wrong password headers = {'Authorization': 'Basic bWVwaGlzdG9waGVsZXM6ZHdlZndlZmVy'} - resp = self.api_call('post', 'user/authenticate', None, False, headers) + resp = self.api_call('post', 'users/authenticate', None, False, headers) self.assertEqual(403, resp.status_code) self.assertEqual({'code': 2, 'message': 'The credentials are invalid.'}, resp.json()) # Try to authenticate as the new user: fails because not active by default headers = {'Authorization': 'Basic bWVwaGlzdG9waGVsZXM6YmFy'} - resp = self.api_call('post', 'user/authenticate', None, False, headers) + resp = self.api_call('post', 'users/authenticate', None, False, headers) self.assertEqual(403, resp.status_code) self.assertEqual({'code': 3, 'message': 'The user with username = mephistopheles is inactive.'}, resp.json()) # Activate the user and update all the available data payload = {'email': 'fooz', 'password': 'barz', 'username': 'mephistophelesz', 'active': 1} - resp = self.api_call('patch', 'user/' + str(user_id), payload, True) + resp = self.api_call('patch', 'users/' + str(user_id), payload, True) self.assertEqual(200, resp.status_code) self.assertEqual(True, resp.json()["active"]) @@ -77,13 +77,13 @@ def test_creation_update_activate_renew_token(self): # Try to authenticate as the new user: success headers = {'Authorization': 'Basic bWVwaGlzdG9waGVsZXN6OmJhcno='} - resp = self.api_call('post', 'user/authenticate', None, False, headers) + resp = self.api_call('post', 'users/authenticate', None, False, headers) self.assertEqual(200, resp.status_code) token = resp.json()["token"] # Renew my token - resp = self.api_call('post', 'user/renew-token', payload, False, {'Authorization': 'token ' + token}) + resp = self.api_call('post', 'users/renew-token', payload, False, {'Authorization': 'token ' + token}) self.assertEqual(200, resp.status_code) self.assertEqual(True, resp.json()["active"]) @@ -92,41 +92,41 @@ def test_creation_update_activate_renew_token(self): self.assertEqual(user_id, resp.json()["id"]) # Try again: fail because the token has been properly changed - resp = self.api_call('post', 'user/renew-token', payload, False, {'Authorization': 'token ' + token}) + resp = self.api_call('post', 'users/renew-token', payload, False, {'Authorization': 'token ' + token}) self.assertEqual(403, resp.status_code) self.assertEqual({'message': 'Token is invalid', 'code': 13}, resp.json()) # Try to create the same user: fails because already exists payload = {'email': 'fooz', 'password': 'barbar', 'username': 'mephistophelesZ'} - resp = self.api_call('post', 'user', payload, True) + resp = self.api_call('post', 'users', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'user' with email 'fooz' already exists.", 'code': 8}, resp.json()) # Try to update: fails because already exists payload = {'email': 'fooz', 'password': 'barz', 'username': 'a_new_username', 'status': 1} - resp = self.api_call('patch', 'user/1', payload, True) + resp = self.api_call('patch', 'users/1', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'user' with email 'fooz' already exists.", 'code': 8}, resp.json()) def test_get_by_filter_fails_unknown_filter(self): - resp = self.api_call('get', 'user?filter=toto', {}, True) + resp = self.api_call('get', 'users?filter=toto', {}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following filter is not allowed: toto. Allowed filters are: id, email, username.', 'code': 10}, resp.json()) def test_get_by_filter_no_result(self): - resp = self.api_call('get', 'user?filter=username&value=toto', {}, True) + resp = self.api_call('get', 'users?filter=username&value=toto', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'user' with username 'toto' has not been found.", 'code': 1}, resp.json()) def test_get_by_filters(self): - resp = self.api_call('get', 'user?filter=username&value=Eric', {}, True) + resp = self.api_call('get', 'users?filter=username&value=Eric', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'active': True, 'email': 'foo@bar.com', 'id': 1, 'username': 'Eric'}, resp.json()) - resp = self.api_call('get', 'user?filter=email&value=foo@bar.com', {}, True) + resp = self.api_call('get', 'users?filter=email&value=foo@bar.com', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'active': True, 'email': 'foo@bar.com', 'id': 1, 'username': 'Eric'}, resp.json()) - resp = self.api_call('get', 'user?filter=id&value=1', {}, True) + resp = self.api_call('get', 'users?filter=id&value=1', {}, True) self.assertEqual(200, resp.status_code) self.assertEqual({'active': True, 'email': 'foo@bar.com', 'id': 1, 'username': 'Eric'}, resp.json()) diff --git a/test/functional/test_versions.py b/test/functional/test_versions.py index 23889bc..b91ce92 100644 --- a/test/functional/test_versions.py +++ b/test/functional/test_versions.py @@ -2,18 +2,18 @@ class TestVersions(AbstractTests): def test_commons(self): - super().check_all_routes_error_bad_user_token('version') - super().check_all_routes_error_missing_user_token('version') + super().check_all_routes_error_bad_user_token('versions') + super().check_all_routes_error_missing_user_token('versions') def test_get_version(self): # Does not exist - resp = self.api_call('get', 'version/666', {}, True) + resp = self.api_call('get', 'versions/666', {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'version' with id #666 has not been found.", 'code': 1}, resp.json()) # Exist - resp = self.api_call('get', 'version/1', {}, True) + resp = self.api_call('get', 'versions/1', {}, True) self.assertEqual(200, resp.status_code) expected_result = { @@ -76,7 +76,7 @@ def test_create_incomplete_payload(self): "todoWithHelp": False, "topGame": False } - resp = self.api_call('post', 'version', payload, True) + resp = self.api_call('post', 'versions', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': 'The following field is missing: platformId.', 'code': 6}, resp.json()) @@ -107,7 +107,7 @@ def test_create_fails_platform_not_found(self): "todoWithHelp": False, "topGame": False } - resp = self.api_call('post', 'version', payload, True) + resp = self.api_call('post', 'versions', payload, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'platform' with id #700 has not been found.", 'code': 1}, resp.json()) @@ -138,7 +138,7 @@ def test_create_fails_game_not_found(self): "todoWithHelp": False, "topGame": False } - resp = self.api_call('post', 'version', payload, True) + resp = self.api_call('post', 'versions', payload, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'game' with id #1000 has not been found.", 'code': 1}, resp.json()) @@ -169,14 +169,14 @@ def test_create_fails_duplicate_platform_game_couple(self): "todoWithHelp": False, "topGame": False } - resp = self.api_call('post', 'version', payload, True) + resp = self.api_call('post', 'versions', payload, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'platform-game couple' with id #7:1 already exists.", 'code': 8}, resp.json()) def test_create_update_delete_success(self): # Create the game - resp = self.api_call('post', 'game', {'finished': True, 'title': 'Something'}, True) + resp = self.api_call('post', 'games', {'finished': True, 'title': 'Something'}, True) self.assertEqual(200, resp.status_code) self.assertEqual('Something', resp.json()["title"]) @@ -213,7 +213,7 @@ def test_create_update_delete_success(self): } # Create - resp = self.api_call('post', 'version', payload, True) + resp = self.api_call('post', 'versions', payload, True) self.assertEqual(200, resp.status_code) @@ -231,7 +231,7 @@ def test_create_update_delete_success(self): str_id = str(resp.json()["id"]) str_game_id = str(resp.json()["gameId"]) - resp = self.api_call('patch', 'version/' + str_id, {'topGame': True}, True) + resp = self.api_call('patch', 'versions/' + str_id, {'topGame': True}, True) self.assertEqual(200, resp.status_code) self.assertEqual(True, resp.json()["topGame"]) @@ -239,61 +239,61 @@ def test_create_update_delete_success(self): self.assertEqual(0, resp.json()["toWatchPosition"]) # Delete - resp = self.api_call('delete', 'version/' + str_id, {}, True) + resp = self.api_call('delete', 'versions/' + str_id, {}, True) self.assertEqual(200, resp.status_code) - resp = self.api_call('delete', 'version/' + str_id, {}, True) + resp = self.api_call('delete', 'versions/' + str_id, {}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'version' with id #350 has not been found.", 'code': 1}, resp.json()) # Remove the game too (otherwise it will pollute the DB) - resp = self.api_call('delete', 'game/' + str_game_id, {}, True) + resp = self.api_call('delete', 'games/' + str_game_id, {}, True) self.assertEqual(200, resp.status_code) def test_update_fails_because_resource_not_found(self): - resp = self.api_call('patch', 'version/9999', {'topGame': True}, True) + resp = self.api_call('patch', 'versions/9999', {'topGame': True}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'version' with id #9999 has not been found.", 'code': 1}, resp.json()) def test_update_fails_because_platform_not_found(self): - resp = self.api_call('patch', 'version/1', {'platformId': 9999}, True) + resp = self.api_call('patch', 'versions/1', {'platformId': 9999}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'platform' with id #9999 has not been found.", 'code': 1}, resp.json()) def test_update_fails_because_game_not_found(self): - resp = self.api_call('patch', 'version/1', {'gameId': 9999}, True) + resp = self.api_call('patch', 'versions/1', {'gameId': 9999}, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'game' with id #9999 has not been found.", 'code': 1}, resp.json()) def test_update_fails_because_platform_game_couple_already_exist(self): - resp = self.api_call('patch', 'version/347', {'platformId': 8, 'gameId': 377}, True) + resp = self.api_call('patch', 'versions/347', {'platformId': 8, 'gameId': 377}, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The resource of type 'platform-game couple' with id #377-8 already exists.", 'code': 8}, resp.json()) def test_delete_fails_because_not_found(self): - resp = self.api_call('delete', 'version/9999', None, True) + resp = self.api_call('delete', 'versions/9999', None, True) self.assertEqual(404, resp.status_code) self.assertEqual({'message': "The resource of type 'version' with id #9999 has not been found.", 'code': 1}, resp.json()) def test_delete_fails_because_has_copies(self): - resp = self.api_call('delete', 'version/349', None, True) + resp = self.api_call('delete', 'versions/349', None, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The following resource type 'version' has children of type 'copy', so it cannot be deleted.", 'code': 9}, resp.json()) def test_delete_fails_because_has_stories(self): - resp = self.api_call('delete', 'version/231', None, True) + resp = self.api_call('delete', 'versions/231', None, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The following resource type 'version' has children of type 'story', so it cannot be deleted.", 'code': 9}, resp.json()) def test_delete_fails_because_has_transactions(self): - resp = self.api_call('delete', 'version/340', None, True) + resp = self.api_call('delete', 'versions/340', None, True) self.assertEqual(400, resp.status_code) self.assertEqual({'message': "The following resource type 'version' has children of type 'transaction', so it cannot be deleted.", 'code': 9}, resp.json())