diff --git a/.gitignore b/.gitignore index 35320c42..5b534cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ __pycache__/ .venv/ smoketest-report.xml -env +.env .dir-locals.el *.pyc */package-lock.json @@ -38,5 +38,7 @@ test-report.xml .coverage localstack_data/ +sandbox/specification/* /volume/* /coverage.xml +/specification/tmp/* diff --git a/Makefile b/Makefile index 5c42e288..82ecce9b 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,6 @@ install-python: #Installs dependencies using npm. install-node: npm install --legacy-peer-deps - cd sandbox && npm install --legacy-peer-deps #Configures Git Hooks, which are scripts that run given a specified event. .git/hooks/pre-commit: @@ -34,8 +33,9 @@ format: ## Format and fix code #Creates the fully expanded OAS spec in json publish: clean mkdir -p build + mkdir -p sandbox/specification npm run publish 2> /dev/null - + cp build/eligibility-signposting-api.json sandbox/specification/eligibility-signposting-api.json #Files to loop over in release _dist_include="pytest.ini poetry.lock poetry.toml pyproject.toml Makefile build/. tests" @@ -48,9 +48,6 @@ dependencies: # Install dependencies needed to build and test the project @Pipel build: # Build lambda in dist poetry build-lambda -vv -publish: # Publish the project artefact @Pipeline - # TODO: Implement the artefact publishing step - deploy: # Deploy the project artefact to the target environment @Pipeline # TODO: Implement the artefact deployment step diff --git a/README.md b/README.md index 3e1d96b1..227acc7c 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,11 @@ The software will only be used for signposting an individual to an appropriate s - [Setup](#setup) - [Prerequisites](#prerequisites) - [Configuration](#configuration) + - [Environment variables](#environment-variables) - [Usage](#usage) - [Testing](#testing) + - [Sandbox](#sandbox) + - [Conflict with yanai](#conflict-with-yanai) - [Design](#design) - [Diagrams](#diagrams) - [Modularity](#modularity) @@ -88,6 +91,10 @@ make precommit There are `make` tasks for you to configure to run your tests. Run `make test` to see how they work. You should be able to use the same entry points for local development as in your CI pipeline. +## Sandbox + +There is a minimalist sandbox environment in `/sandbox` with an accompanying README with instructions on how to run it locally. + ## Conflict with yanai If you have previously built [yanai](https://nhsd-confluence.digital.nhs.uk/pages/viewpage.action?pageId=48826732), which is the platform we use to supply data to this project, that uses an old version of localstack that does not support our Python version. We have pinned the correct version here and yanai have their version pinned as well so it should work fine, but sometimes issues can arise - if so then removing the docker image can solve that, before then rebuilding. diff --git a/pyproject.toml b/pyproject.toml index 3838a4a1..ac277abc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ log_date_format = "%Y-%m-%d %H:%M:%S" [tool.coverage.run] relative_files = true branch = true -source = ["src"] +source = ["src", "sandbox"] [tool.coverage.report] show_missing = true diff --git a/sandbox/.dockerignore b/sandbox/.dockerignore deleted file mode 100644 index 93f13619..00000000 --- a/sandbox/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -npm-debug.log diff --git a/sandbox/.eslintrc.js b/sandbox/.eslintrc.js deleted file mode 100644 index 5ba967d8..00000000 --- a/sandbox/.eslintrc.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - "env": { - "commonjs": true, - "es6": true, - "node": true - }, - "extends": "eslint:recommended", - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 2018 - }, - "rules": { - } -}; diff --git a/sandbox/.mocharc.yml b/sandbox/.mocharc.yml deleted file mode 100644 index bfafc399..00000000 --- a/sandbox/.mocharc.yml +++ /dev/null @@ -1,4 +0,0 @@ -spec: './*.spec.js' -ui: bdd -watch-files: - - '**/*.js' diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index 391cfecc..22f76820 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -1,11 +1,38 @@ -FROM node:12 +# First Stage: Build Proxy service +FROM python:3.13-alpine AS deps -COPY . /sandbox +# Install poetry +RUN pip install poetry -WORKDIR /sandbox +# Install python dependencies in /.venv +COPY pyproject.toml poetry.lock ./ +RUN poetry config virtualenvs.in-project true && poetry install --no-root -RUN npm install +FROM deps AS runtime -EXPOSE 9000 +# Copy virtual env from python-deps stage +COPY --from=deps /.venv /.venv +ENV PATH="/.venv/bin:$PATH" + +# Create and switch to a new user +RUN addgroup -S appgroup && \ + adduser -S appuser -G appgroup && \ + mkdir /home/appuser/app +WORKDIR /home/appuser/app +USER appuser -CMD ["npm", "start"] +# Install application into container +COPY app.py __init__.py /home/appuser/app/ +# Copy supervisord configuration +COPY supervisord.conf /etc/supervisord.conf +# Install Prism +USER root +RUN apk add --no-cache nodejs npm && \ + npm install --ignore-scripts -g @stoplight/prism-cli && \ + apk add --no-cache supervisor + +# Expose ports +EXPOSE 9000 +ENV UPSTREAM_HOST=http://localhost:5000 +# Run supervisord +CMD ["supervisord", "-c", "/etc/supervisord.conf"] diff --git a/sandbox/HealthStatusEndpoint.json b/sandbox/HealthStatusEndpoint.json new file mode 100644 index 00000000..67470643 --- /dev/null +++ b/sandbox/HealthStatusEndpoint.json @@ -0,0 +1,21 @@ +{ + "/_status": { + "get": { + "operationId": "healthcheck", + "summary": "healthcheck endpoint", + "responses": { + "200": { + "description": "Successful Operation", + "content": { + "application/text": { + "schema": { + "type": "string" + }, + "example": "OK" + } + } + } + } + } + } + } diff --git a/sandbox/Makefile b/sandbox/Makefile index 01ee36da..637da37c 100644 --- a/sandbox/Makefile +++ b/sandbox/Makefile @@ -1,11 +1,18 @@ SHELL := /bin/bash dirname := $(notdir $(patsubst %/,%,$(CURDIR))) +project_name = eligibility-signposting-api list: @grep '^[^#[:space:]].*:' Makefile build: - docker build . + docker-compose -f docker-compose.yaml build + +up: build + docker-compose -f docker-compose.yaml up -d + +down: + docker-compose -f docker-compose.yaml down clean: rm -rf ./node_modules @@ -16,8 +23,11 @@ install: update: npm update +spec: + mkdir -p specification + make -C .. publish + test: - NODE_ENV=test npx mocha --reporter spec + UPSTREAM_HOST=test poetry run pytest -test-report: - NODE_ENV=test npx mocha --reporter mocha-junit-reporter --reporter-options mochaFile=../../reports/tests/$(dirname).xml || true +.PHONY: build run spec test diff --git a/sandbox/README.md b/sandbox/README.md new file mode 100644 index 00000000..a3ccee14 --- /dev/null +++ b/sandbox/README.md @@ -0,0 +1,34 @@ +# Sandbox environment + +The sandbox environment uses: + +* [OpenAPI Generator CLI](https://github.com/OpenAPITools/openapi-generator-cli) to validate the specification and convert it from .yaml to .json for use in the sandbox. +* [Prism](https://stoplight.io/open-source/prism) as a mock server. +* A flask proxy to allow us to inject specific examples based on request attributes. + +## Developer instructions + +Run the following command to start the sandbox environment: + +```bash +make spec +make up +``` + +This will start the sandbox environment on localhost port 5000. + +```bash +make down +``` + +This will stop the sandbox environment. + +### Example curl calls + +patient 0000000001 is a patient eligible and bookable for a Flu vaccination. + +```bash + curl -X GET "http://0.0.0.0:9000/eligibility?patient=0000000001" -H "Accept: application/json" -H "Authorization: Bearer sdvsd" +``` + +See [app.py](app.py) for more examples. diff --git a/sandbox/__init__.py b/sandbox/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sandbox/app.js b/sandbox/app.js deleted file mode 100644 index b546b0a5..00000000 --- a/sandbox/app.js +++ /dev/null @@ -1,133 +0,0 @@ -"use strict"; - -const express = require("express"); -const app = express(); -const log = require("loglevel"); -const uuid = require('uuid'); - - - -function setup(options) { - options = options || {}; - app.locals.app_name = options.APP_NAME || 'eligibility-signposting-api'; - app.locals.version_info = JSON.parse(options.VERSION_INFO || '{}'); - log.setLevel(options.LOG_LEVEL || "info"); - - - log.info(JSON.stringify({ - timestamp: Date.now(), - level: "info", - app: app.locals.app_name, - msg: "setup", - version: app.locals.version_info - })); -} - -function start(options) { - options = options || {}; - let server = app.listen(options.PORT || 9000, () => { - log.info(JSON.stringify({ - timestamp: Date.now(), - level: "info", - app: app.locals.app_name, - msg: "startup", - server_port: server.address().port, - version: app.locals.version_info - })) - }); - return server; -} - -function before_request(req, res, next) { - res.locals.started_at = Date.now(); - res.locals.correlation_id = ( - req.header('X-Correlation-ID') - || req.header('Correlation-ID') - || req.header('CorrelationID') - || uuid.v4() - ); - next(); -} - -const _health_endpoints = ["/_ping", "/health"]; - -function after_request(req, res, next) { - if (_health_endpoints.includes(req.path) && !('log' in Object.assign({}, req.query))) { - // don't log ping / health by default - return next(); - } - let finished_at = Date.now(); - let log_entry = { - timestamp: finished_at, - level: "info", - app: app.locals.app_name, - msg: "request", - correlation_id: res.locals.correlation_id, - started: res.locals.started_at, - finished: finished_at, - duration: finished_at - res.locals.started_at, - req: { - url: req.url, - method: req.method, - query: req.query, - path: req.path, - }, - res: { - status: res.statusCode, - message: res.message - }, - version: app.locals.version_info - }; - - if (log.getLevel()<2) { - // debug - log_entry.req.headers = req.rawHeaders; - log_entry.res.headers = res.rawHeaders; - } - log.info(JSON.stringify(log_entry)); - - next(); - -} - -function on_error(err, req, res, next) { - let log_err = err; - if (log_err instanceof Error) { - log_err = { - name: err.name, - message: err.message, - stack: err.stack - } - } - let finished_at = Date.now(); - log.error(JSON.stringify({ - timestamp: finished_at, - level: "error", - app: app.locals.app_name, - msg: "error", - correlation_id: res.locals.correlation_id, - started: res.locals.started_at, - finished: finished_at, - duration: finished_at - res.locals.started_at, - err: log_err, - version: app.locals.version_info - })); - if (res.headersSent) { - next(); - return; - } - res.status(500); - res.json({error: "something went wrong" }); - next(); -} - -const handlers = require("./handlers"); -app.use(before_request); -app.get("/_ping", handlers.status); -app.get("/_status", handlers.status); -app.get("/health", handlers.status); -app.all("/hello", handlers.hello); -app.use(on_error) -app.use(after_request); - -module.exports = {start: start, setup: setup}; diff --git a/sandbox/app.py b/sandbox/app.py new file mode 100644 index 00000000..37e5d2d0 --- /dev/null +++ b/sandbox/app.py @@ -0,0 +1,100 @@ +""" +Proxy server for Stoplight Prism with response example selection logic. + +Adapted from https://stackoverflow.com/a/36601467 +""" + +import logging +import os +import sys + +import requests # pyright: ignore [reportMissingModuleSource] +from flask import Flask, Request, Response, request # pyright: ignore [reportMissingImports] + +# Configure logging to output to stdout +logging.basicConfig(stream=sys.stdout, level=logging.INFO) + +# HTTP proxy to Prism +UPSTREAM_HOST = os.environ.get("UPSTREAM_HOST") +if not UPSTREAM_HOST: + NO_UPSTREAM_HOST = "UPSTREAM_HOST environment variable not set" + raise ValueError(NO_UPSTREAM_HOST) + +app = Flask(__name__) +app.logger.setLevel("INFO") +session = requests.Session() + +HOP_BY_HOP_HEADERS = [ + "connection", + "content-encoding", + "content-length", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", +] + +PATIENT_EXAMPLES = { + "patient=0000000001": "example1", + "patient=0000000002": "example2", + "patient=0000000003": "code404", +} + + +def exclude_hop_by_hop(headers: dict) -> list[tuple[str, str]]: + """ + Exclude hop-by-hop headers, which are meaningful only for a single + transport-level connection, and are not stored by caches or forwarded by + proxies. See https://www.rfc-editor.org/rfc/rfc2616#section-13.5.1. + """ + return [(k, v) for k, v in headers.items() if k.lower() not in HOP_BY_HOP_HEADERS] + + +def get_prism_prompt_for_example(patient_examples: dict, request: Request) -> str | None: + """ + Given the whole request, return the `Prefer:` header value if a specific + example is desired. Otherwise, return `None`. + """ + for patient_id, example in patient_examples.items(): + if patient_id in request.full_path: + return example + return None + + +def parse_prefer_header_value(prefer_header_value: str) -> str: + """ + Parse the Prefer header value to extract the example name. + """ + if prefer_header_value.startswith("example"): + return f"example={prefer_header_value}" + if prefer_header_value.startswith("code"): + return f"code={prefer_header_value[4:]}" + return "" + + +@app.route("/", defaults={"path": ""}) +@app.route("/") +def proxy_to_upstream(path: str) -> Response: # noqa: ARG001 + headers_to_upstream = {k: v for k, v in request.headers if k.lower() != "host"} + + prefer_header_value = get_prism_prompt_for_example(PATIENT_EXAMPLES, request) + if prefer_header_value: + headers_to_upstream["prefer"] = parse_prefer_header_value(prefer_header_value) + + request_to_upstream = requests.Request( + method=request.method, + url=request.url.replace(request.host_url, UPSTREAM_HOST + "/"), # pyright: ignore [reportOptionalOperand] + headers=headers_to_upstream, + data=request.get_data(), + cookies=request.cookies, + ).prepare() + response_from_upstream = session.send(request_to_upstream) + + return Response( + response_from_upstream.content, + response_from_upstream.status_code, + exclude_hop_by_hop(response_from_upstream.raw.headers), + ) diff --git a/sandbox/app.spec.js b/sandbox/app.spec.js deleted file mode 100644 index a783607f..00000000 --- a/sandbox/app.spec.js +++ /dev/null @@ -1,66 +0,0 @@ - -const request = require("supertest"); -const assert = require("chai").assert; -// const expect = require("chai").expect; - - -describe("app handler tests", function () { - let server; - let env; - const version_info = { - build_label:"1233-shaacdef1", - releaseId:"1234", - commitId:"acdef12341ccc" - }; - - before(function () { - env = process.env; - let app = require("./app"); - app.setup({ - VERSION_INFO: JSON.stringify(version_info), - LOG_LEVEL: (process.env.NODE_ENV === "test" ? "warn": "debug") - }); - server = app.start(); - }); - - beforeEach(function () { - - }); - afterEach(function () { - - }); - after(function () { - process.env = env; - server.close(); - }); - - it("responds to /_ping", (done) => { - request(server) - .get("/_ping") - .expect(200, { - status: "pass", - ping: "pong", - service: "eligibility-signposting-api", - version: version_info - }) - .expect("Content-Type", /json/, done); - }); - - it("responds to /_status", (done) => { - request(server) - .get("/_status") - .expect(200, { - status: "pass", - ping: "pong", - service: "eligibility-signposting-api", - version: version_info - }) - .expect("Content-Type", /json/, done); - }); - - it("responds to /hello", (done) => { - request(server) - .get("/hello") - .expect(200, done); - }); -}); diff --git a/sandbox/docker-compose.yaml b/sandbox/docker-compose.yaml new file mode 100644 index 00000000..c84eb15f --- /dev/null +++ b/sandbox/docker-compose.yaml @@ -0,0 +1,10 @@ +services: + combined-service: + container_name: combined-service + build: + context: . + dockerfile: Dockerfile # This will use the combined Dockerfile + ports: + - "9000:9000" # Port for your custom Prism service + volumes: + - ./specification:/home/appuser/app/specification # For hot-reloading the example-chooser (optional) diff --git a/sandbox/handlers.js b/sandbox/handlers.js deleted file mode 100644 index b920733e..00000000 --- a/sandbox/handlers.js +++ /dev/null @@ -1,68 +0,0 @@ -"use strict"; - -const log = require("loglevel"); - - -const write_log = (res, log_level, options = {}) => { - if (log.getLevel()>log.levels[log_level.toUpperCase()]) { - return - } - if (typeof options === "function") { - options = options() - } - let log_line = { - timestamp: Date.now(), - level: log_level, - correlation_id: res.locals.correlation_id - } - if (typeof options === 'object') { - options = Object.keys(options).reduce(function(obj, x) { - let val = options[x] - if (typeof val === "function") { - val = val() - } - obj[x] = val; - return obj; - }, {}); - log_line = Object.assign(log_line, options) - } - if (Array.isArray(options)) { - log_line["log"] = {log: options.map(x=> {return typeof x === "function"? x() : x })} - } - - log[log_level](JSON.stringify(log_line)) -}; - - -async function status(req, res, next) { - res.json({ - status: "pass", - ping: "pong", - service: req.app.locals.app_name, - version: req.app.locals.version_info - }); - res.end(); - next(); -} - -async function hello(req, res, next) { - - write_log(res, "warn", { - message: "hello world", - req: { - path: req.path, - query: req.query, - headers: req.rawHeaders - } - }); - - - res.json({message: "hello world"}); - res.end(); - next(); -} - -module.exports = { - status: status, - hello: hello -}; diff --git a/sandbox/index.js b/sandbox/index.js deleted file mode 100644 index 6b425200..00000000 --- a/sandbox/index.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; - -const app = require("./app"); - -app.setup(process.env); - -const server = app.start(process.env); - - -const signals = { - 'SIGHUP': 1, - 'SIGINT': 2, - 'SIGTERM': 15 -}; - -const shutdown = (signal, value) => { - console.log("shutdown!"); - server.close(() => { - console.log(`server stopped by ${signal} with value ${value}`); - process.exit(128 + value); - }); -}; - -Object.keys(signals).forEach((signal) => { - process.on(signal, () => { - console.log(`process received a ${signal} signal`); - shutdown(signal, signals[signal]); - }); -}); - - -module.exports = {server: server}; diff --git a/sandbox/package.json b/sandbox/package.json deleted file mode 100644 index 065cb07a..00000000 --- a/sandbox/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "eligibility-signposting-api", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "start": "node index.js" - }, - "license": "Unlicense", - "private": true, - "dependencies": { - "express": "^5.0.0-alpha.8", - "loglevel": "^1.7.1", - "uuid": "^8.3.2" - }, - "devDependencies": { - "chai": "^4 .2.0", - "mocha": "<7.0.0", - "mocha-junit-reporter": "^1.23.3", - "mocha-multi": "^1.1.3", - "nodemon": "^2.0.7", - "supertest": "^4.0.2" - } -} diff --git a/sandbox/poetry.lock b/sandbox/poetry.lock new file mode 100644 index 00000000..cb477cd0 --- /dev/null +++ b/sandbox/poetry.lock @@ -0,0 +1,436 @@ +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. + +[[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 = "2025.1.31" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[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 = "flask" +version = "3.1.0" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, + {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, +] + +[package.dependencies] +blinker = ">=1.9" +click = ">=8.1.3" +itsdangerous = ">=2.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.1" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[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.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pyhamcrest" +version = "2.1.0" +description = "Hamcrest framework for matcher objects" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pyhamcrest-2.1.0-py3-none-any.whl", hash = "sha256:f6913d2f392e30e0375b3ecbd7aee79e5d1faa25d345c8f4ff597665dcac2587"}, + {file = "pyhamcrest-2.1.0.tar.gz", hash = "sha256:c6acbec0923d0cb7e72c22af1926f3e7c97b8e8d69fc7498eabacaf7c975bd9c"}, +] + +[package.extras] +dev = ["black", "doc2dash", "flake8", "pyhamcrest[docs,tests]", "pytest-mypy", "towncrier", "tox", "tox-asdf", "twine"] +docs = ["alabaster (>=0.7,<1.0)", "sphinx (>=4.0,<5.0)"] +tests = ["coverage[toml]", "dataclasses ; python_version < \"3.7\"", "mypy (!=0.940) ; platform_python_implementation != \"PyPy\"", "pytest (>=5.0)", "pytest-mypy-plugins ; platform_python_implementation != \"PyPy\"", "pytest-sugar", "pytest-xdist", "pyyaml", "types-dataclasses ; python_version < \"3.7\"", "types-mock"] +tests-numpy = ["numpy", "pyhamcrest[tests]"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "urllib3" +version = "2.3.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.13" +content-hash = "be1bc3e9d5eb541f5a8fb709eb8e360e327cac4f2d445c4f3718e1423794b7b0" diff --git a/sandbox/pyproject.toml b/sandbox/pyproject.toml new file mode 100644 index 00000000..b1c96815 --- /dev/null +++ b/sandbox/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "sandbox" +version = "0.1.0" +description = "Eligibility Sandbox API" +authors = [ + {name = "Edd Almond",email = "edd.almond1@nhs.net"} +] +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "flask (>=3.1.0,<4.0.0)", + "requests (>=2.31.0,<3.0.0)", +] +package-mode = false + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.0" +pyhamcrest = "^2.1.0" diff --git a/sandbox/supervisord.conf b/sandbox/supervisord.conf new file mode 100644 index 00000000..3dedb825 --- /dev/null +++ b/sandbox/supervisord.conf @@ -0,0 +1,19 @@ +[supervisord] +nodaemon=true +user=root + +[program:prism] +command=/usr/local/bin/prism mock -h 0.0.0.0 -p 5000 /home/appuser/app/specification/eligibility-signposting-api.json +user=appuser +autostart=true +autorestart=true +stdout_logfile=/var/log/prism.log +stderr_logfile=/var/log/prism_err.log + +[program:proxy] +command=python -m flask run --host=0.0.0.0 --port=9000 +user=appuser +autostart=true +autorestart=true +stdout_logfile=/var/log/proxy.log +stderr_logfile=/var/log/proxy_err.log diff --git a/sandbox/tests/__init__.py b/sandbox/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sandbox/tests/app_tests.py b/sandbox/tests/app_tests.py new file mode 100644 index 00000000..ae9d9ea1 --- /dev/null +++ b/sandbox/tests/app_tests.py @@ -0,0 +1,76 @@ +from unittest.mock import Mock + +import pytest # pyright: ignore [reportMissingImports] +from hamcrest import assert_that, equal_to # pyright: ignore [reportMissingImports] + +from sandbox.app import exclude_hop_by_hop, get_prism_prompt_for_example, parse_prefer_header_value + +HOP_BY_HOP_HEADERS = [ + "connection", + "content-encoding", + "content-length", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", +] + + +@pytest.mark.parametrize( + ("headers", "expected"), + [ + ( + { + "Connection": "keep-alive", + "Content-Encoding": "gzip", + "Content-Length": "1234", + "Keep-Alive": "timeout=5, max=1000", + "Proxy-Authenticate": "Basic", + "Proxy-Authorization": "Basic", + "TE": "trailers", + "Trailers": "value", + "Transfer-Encoding": "chunked", + "Upgrade": "h2c", + "Custom-Header": "custom-value", + }, + [("Custom-Header", "custom-value")], + ), + ], +) +def test_exclude_hop_by_hop(headers: dict, expected: dict) -> None: + assert_that(exclude_hop_by_hop(headers), equal_to(expected)) + + +@pytest.mark.parametrize( + ("full_path", "expected"), + [ + ("/api/resource?patient=0000000001", "example1"), + ("/api/resource?patient=0000000002", "example2"), + ("/api/resource?patient=0000000003", "code404"), + ("/api/resource?patient=0000000004", None), + ], +) +def test_get_prism_prompt_for_example(full_path: str, expected: str) -> None: + patient_examples = { + "patient=0000000001": "example1", + "patient=0000000002": "example2", + "patient=0000000003": "code404", + } + request = Mock() + request.full_path = full_path + assert_that(get_prism_prompt_for_example(patient_examples, request), equal_to(expected)) + + +@pytest.mark.parametrize( + ("prefer_header_value", "expected"), + [ + ("example1", "example=example1"), + ("code404", "code=404"), + ("unknown", ""), + ], +) +def test_parse_prefer_header_value(prefer_header_value: str, expected: str) -> None: + assert_that(parse_prefer_header_value(prefer_header_value), equal_to(expected)) diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index 5764005e..1621ae28 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -16,6 +16,6 @@ cd "$(git rev-parse --show-toplevel)" # or whatever is appropriate to your project. You should *only* run your fast # tests from here. If you want to run other test suites, see the predefined # tasks in scripts/test.mk. - +UPSTREAM_HOST=test make dependencies install-python -poetry run pytest tests/unit/ --durations=10 --cov-report=xml --cov=src/ +UPSTREAM_HOST=$UPSTREAM_HOST poetry run pytest tests/unit/ sandbox/tests/ --durations=10 --cov-report=xml --cov=src/ diff --git a/specification/eligibility-signposting-api.yaml b/specification/eligibility-signposting-api.yaml index 4690ad75..5ea0695a 100644 --- a/specification/eligibility-signposting-api.yaml +++ b/specification/eligibility-signposting-api.yaml @@ -1,46 +1,220 @@ -# This is an OpenAPI Specification (https://swagger.io/specification/) -# for eligibility-signposting-api owned by NHS Digital (https://digital.nhs.uk/) -openapi: '3.0.0' +openapi: 3.0.1 info: - title: 'eligibility-signposting-api' - version: 'Computed and injected at build time by `scripts/set_version.py`' - description: | - ## Overview - Add a fully fledged description here with markdown syntax. - For help completing the content of your API spec, see [our guidance](https://nhsd-confluence.digital.nhs.uk/display/APM/Documenting+your+API). - You should also base your content on our exemplar API - [PDS FHIR](https://digital.nhs.uk/developer/api-catalogue/personal-demographics-service-fhir). - ## Who can use this API - To be completed. - ## Related APIs - To be completed. - ## API status and roadmap - To be completed. - ## Service level - To be completed. - ## Technology - To be completed. - ## Network access - To be completed. - ## Security and authorisation - To be completed. - ## Errors - To be completed. - ### Open source - To be completed. - ## Environments and testing - To be completed. - ## Onboarding - To be completed. - + title: Eligibility Signposting API + version: 1.0.0-alpha + description: API to assess eligibility based on category, condition and NHS number. contact: - name: 'eligibility-signposting-api API Support' - url: 'https://digital.nhs.uk/developer/help-and-support' - email: api.management@nhs.net + url: https://developer.nhs.uk/apis/eligibility-signposting-api + termsOfService: https://developer.nhs.uk/apis/eligibility-signposting-api +tags: + - name: Eligibility + - name: Signposting + - name: NextActions servers: - - url: 'https://sandbox.api.service.nhs.uk/eligibility-signposting-api' - description: Sandbox environment. - - url: 'https://int.api.service.nhs.uk/eligibility-signposting-api' - description: Integration test environment. - - url: 'https://api.service.nhs.uk/eligibility-signposting-api' - description: Production environment. -paths: {} + - url: https://sandbox.api.service.nhs.uk/eligibility-signposting + description: Sandbox Server + - url: https://int.api.service.nhs.uk/eligibility-signposting + description: Integration Server + - url: https://api.service.nhs.uk/eligibility-signposting + description: Production Server +security: + - OAuth_Token: [] +paths: + /eligibility: + get: + summary: Check Eligibility + description: >- + Determines which suggestions a person is eligible for and which they are + not, including reasons and next steps. + operationId: checkEligibility + parameters: + - name: patient + in: query + required: true + schema: + type: string + example: '9876543210' + description: The NHS number of the person. + - name: category + in: query + required: false + schema: + type: string + example: VACCINATIONS + default: ALL + description: >- + The category for which the caller is checking eligibility + suggestions. If not provided, eligibility for all supported + categories will be checked. + - name: conditions + in: query + required: false + schema: + type: string + example: FLU,RSV + default: ALL + description: >- + The diseases or conditions for which the caller is checking + eligibility suggestions in a comma separated list. If not provided, eligibility for all + supported diseases will be checked. + responses: + '200': + description: Eligibility response. + content: + application/json: + schema: + type: object + properties: + responseId: + type: guid + description: GUID assigned when the decisioning evaluation is carried out. This will be useful if you ever need to request support for a particular request. This will not change if you receive a cached response. + example: 1a233ba5-e1eb-4080-a086-2962f6fc3473 + processedDateTime: + type: string + description: Timestamp of when the decisioning evaluation is carried out. This will not change if you receive a cached response. + example: '2025-02-12T16:11:22Z' + processedSuggestions: + type: array + description: List of suggestions the person is eligible for. + items: + type: object + properties: + condition: + type: string + example: FLU + status: + type: string + example: Bookable + reasons: + type: array + description: Reasons that the status returned was returned. + items: + type: object + properties: + reasonCode: + type: string + reasonText: + type: string + example: + - reasonCode: 'flu_coded_eligibility' + reasonText: Our records indicate that you might be at a higher risk of illness if you were to catch Flu. + - reasonCode: 'flu_65+_autumnwinter2023' + reasonText: Our records indicate you are over 65 + nextSteps: + type: array + description: List of next steps for the person. + items: + type: object + properties: + actionType: + type: string + description: >- + The type of step (e.g., information, link, + button). + actionCode: + type: string + description: Code representing the action to be taken + description: + type: string + description: A brief description of the step. + markdownText: + type: string + description: Additional information in markdown format. + htmlText: + type: string + description: Additional information in HTML format. + actionDate: + type: string + description: Optional date by which this action should be undertaken + example: + - actionType: "Link" + actionCode: "BOOK" + description: "Book an appointment here" + markdownText: >- + [Click here](https://www.nhs.uk/nhs-services/pharmacies/book-flu-vaccination/) to book your + appointment. + actionDate: "2025-03-31" + - actionType: "INFO" + actionCode: "NHS" + description: "Visit the NHS website for more details." + markdownText: "[Click here](https://www.nhs.uk/vaccinations/flu-vaccine/) to get more information" + htmlText: "Click here to get more information." + examples: + example1: + summary: Example 1 + value: + responseId: 1a233ba5-e1eb-4080-a086-2962f6fc3473 + processedDateTime: '2025-02-12T16:11:22Z' + processedSuggestions: + - condition: FLU + status: Bookable + reasons: + - reasonCode: 'flu_coded_eligibility' + reasonText: Our records indicate that you might be at a higher risk of illness if you were to catch Flu. + - reasonCode: 'flu_65+_autumnwinter2023' + reasonText: Our records indicate you are over 65 + nextSteps: + - actionType: "Link" + actionCode: "BOOK" + description: "Book an appointment here" + markdownText: >- + [Click here](https://www.nhs.uk/nhs-services/pharmacies/book-flu-vaccination/) to book your + appointment. + actionDate: "2025-03-31" + - actionType: "INFO" + actionCode: "NHS" + description: "Visit the NHS website for more details." + markdownText: "[Click here](https://www.nhs.uk/vaccinations/flu-vaccine/) to get more information" + htmlText: "Click here to get more information." + example2: + summary: Example 2 + value: + responseId: 2b344cb6-f2fc-5091-b197-4073f7fd4584 + processedDateTime: '2025-03-15T10:22:33Z' + processedSuggestions: + - condition: RSV + status: Not Bookable + reasons: + - reasonCode: 'rsv_coded_eligibility' + reasonText: Our records indicate that you might be at a higher risk of illness if you were to catch RSV. + - reasonCode: 'rsv_65+_autumnwinter2023' + reasonText: Our records indicate you are over 65 + nextSteps: + - actionType: "Link" + actionCode: "INFO" + description: "Visit the NHS website for more details." + markdownText: "[Click here](https://www.nhs.uk/vaccinations/rsv-vaccine/) to get more information" + htmlText: "Click here to get more information." + '400': + description: Invalid input data. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: + - The values submitted in your request were not valid. + - The condition list was badly formatted or contained unrecognised conditions. + - The category contained an unrecognised value. + '404': + description: Person not found. + content: + application/json: + schema: + type: object + properties: + error: + type: string + example: >- + The given NHS number was not found in our datasets. This + could be because the number is incorrect or some other + reason we cannot process that number. + '500': + description: Internal server error. +components: + securitySchemes: + OAuth_Token: + type: http + scheme: bearer