diff --git a/Dockerfile b/Dockerfile index 20d4337..a121822 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,4 +53,4 @@ ENV PYTHONPATH "${PYTHONPATH}:/app:/app/dependencies" # Change user USER microdata -CMD ["/app/dependencies/bin/gunicorn", "--logger-class", "metadata_service.config.gunicorn.CustomLogger", "metadata_service.app:app", "--workers", "2", "--limit-request-line", "8190"] +CMD ["/app/dependencies/bin/gunicorn", "-k", "uvicorn.workers.UvicornWorker", "--logger-class", "metadata_service.config.gunicorn.CustomLogger", "metadata_service.app:app", "--workers", "2", "--limit-request-line", "8190"] diff --git a/metadata_service/adapter/datastore.py b/metadata_service/adapter/datastore.py index cf7db54..24c0847 100644 --- a/metadata_service/adapter/datastore.py +++ b/metadata_service/adapter/datastore.py @@ -25,7 +25,7 @@ def get_datastore_versions() -> dict: return json.load(f) -def _get_draft_metadata_all(): +def _get_draft_metadata_all() -> dict: metadata_all_file_path = ( f"{DATASTORE_ROOT_DIR}/datastore/metadata_all__DRAFT.json" ) @@ -34,7 +34,7 @@ def _get_draft_metadata_all(): @lru_cache(maxsize=32) -def _get_versioned_metadata_all(version: Version): +def _get_versioned_metadata_all(version: Version) -> dict: file_version = version.to_3_underscored() metadata_all_file_path = ( f"{DATASTORE_ROOT_DIR}/datastore/metadata_all__{file_version}.json" @@ -43,7 +43,7 @@ def _get_versioned_metadata_all(version: Version): return json.load(f) -def get_metadata_all(version: Version) -> str: +def get_metadata_all(version: Version) -> dict: try: if version.is_draft(): return _get_draft_metadata_all() diff --git a/metadata_service/api/metadata_api.py b/metadata_service/api/metadata_api.py index 6f081a0..74be60c 100644 --- a/metadata_service/api/metadata_api.py +++ b/metadata_service/api/metadata_api.py @@ -1,100 +1,73 @@ import logging -from flask import Blueprint, jsonify, request +from fastapi import APIRouter, Depends from metadata_service.api.request_models import NameParam, MetadataQuery from metadata_service.domain import metadata from metadata_service.domain.version import get_version_from_string logger = logging.getLogger() -metadata_api = Blueprint("metadata_api", __name__) +metadata_router = APIRouter() -@metadata_api.get("/metadata/data-store") +@metadata_router.get("/metadata/data-store") def get_data_store(): logger.info("GET /metadata/data-store") + return metadata.find_all_datastore_versions() - response = jsonify(metadata.find_all_datastore_versions()) - response.headers.set("content-language", "no") - return response - -@metadata_api.get("/metadata/data-structures/status") -def get_data_structure_current_status(): - validated_query = NameParam(**request.args) +@metadata_router.get("/metadata/data-structures/status") +def get_data_structure_current_status(validated_query: NameParam = Depends()): logger.info( f"GET /metadata/data-structures/status with name = {validated_query.names}" ) - response = jsonify( - metadata.find_current_data_structure_status( - validated_query.get_names_as_list() - ) + return metadata.find_current_data_structure_status( + validated_query.get_names_as_list() ) - response.headers.set("content-language", "no") - return response - -@metadata_api.post("/metadata/data-structures/status") -def get_data_structure_current_status_as_post(): - validated_body = NameParam(**request.json) +@metadata_router.post("/metadata/data-structures/status") +def get_data_structure_current_status_as_post(validated_body: NameParam): logger.info( f"POST /metadata/data-structures/status with name = {validated_body.names}" ) - response = jsonify( - metadata.find_current_data_structure_status( - validated_body.get_names_as_list() - ) + return metadata.find_current_data_structure_status( + validated_body.get_names_as_list() ) - response.headers.set("content-language", "no") - return response -@metadata_api.get("/metadata/data-structures") -def get_data_structures(): - validated_query = MetadataQuery(**request.args) +@metadata_router.get("/metadata/data-structures") +def get_data_structures( + validated_query: MetadataQuery = Depends(), +): validated_query.include_attributes = True logger.info(f"GET /metadata/data-structures with query: {validated_query}") - - response = jsonify( - metadata.find_data_structures( - validated_query.names, - get_version_from_string(validated_query.version), - validated_query.include_attributes, - validated_query.skip_code_lists, - ) + return metadata.find_data_structures( + validated_query.names_as_list(), + get_version_from_string(validated_query.version), + validated_query.include_attributes, + validated_query.skip_code_lists, ) - response.headers.set("content-language", "no") - return response -@metadata_api.get("/metadata/all-data-structures") +@metadata_router.get("/metadata/all-data-structures") def get_all_data_structures_ever(): logger.info("GET /metadata/all-data-structures") + return metadata.find_all_data_structures_ever() - response = jsonify(metadata.find_all_data_structures_ever()) - response.headers.set("content-language", "no") - return response - -@metadata_api.get("/metadata/all") -def get_all_metadata(): - validated_query = MetadataQuery(**request.args) +@metadata_router.get("/metadata/all") +def get_all_metadata( + validated_query: MetadataQuery = Depends(), +): logger.info(f"GET /metadata/all with version: {validated_query.version}") - - response = jsonify( - metadata.find_all_metadata( - get_version_from_string(validated_query.version), - validated_query.skip_code_lists, - ) + return metadata.find_all_metadata( + get_version_from_string(validated_query.version), + validated_query.skip_code_lists, ) - response.headers.set("content-language", "no") - return response -@metadata_api.get("/languages") +@metadata_router.get("/languages") def get_languages(): logger.info("GET /languages") - - response = jsonify(metadata.find_languages()) - return response + return metadata.find_languages() diff --git a/metadata_service/api/observability.py b/metadata_service/api/observability.py index aef3356..81bb4cc 100644 --- a/metadata_service/api/observability.py +++ b/metadata_service/api/observability.py @@ -1,14 +1,14 @@ -from flask import Blueprint +from fastapi import APIRouter -observability = Blueprint("observability", __name__) +observability_router = APIRouter() -@observability.get("/health/alive") -def alive(): +@observability_router.get("/health/alive") +async def alive(): return "I'm alive!" -@observability.get("/health/ready") -def ready(): +@observability_router.get("/health/ready") +async def ready(): return "I'm ready!" diff --git a/metadata_service/api/request_models.py b/metadata_service/api/request_models.py index cbde062..4cbf036 100644 --- a/metadata_service/api/request_models.py +++ b/metadata_service/api/request_models.py @@ -11,23 +11,11 @@ class MetadataQuery(BaseModel, extra="forbid", validate_assignment=True): - names: List[str] = [] + names: str | None = None version: str include_attributes: bool = False skip_code_lists: bool = False - @field_validator("names", mode="before") - @classmethod - def split_str(cls, names): - if isinstance(names, List): - return names[0].split(",") - elif isinstance(names, str): - return names.split(",") - else: - raise RequestValidationException( - "names field must be a list or a string" - ) - @field_validator("version", mode="before") @classmethod def validate_version(cls, version: str): @@ -38,6 +26,9 @@ def validate_version(cls, version: str): ) return version + def names_as_list(self) -> List[str]: + return [] if self.names is None else self.names.split(",") + class NameParam(BaseModel, extra="forbid"): names: str diff --git a/metadata_service/app.py b/metadata_service/app.py index 4bfd59a..dc5efec 100644 --- a/metadata_service/app.py +++ b/metadata_service/app.py @@ -1,12 +1,12 @@ import logging -import msgpack -from flask import Flask, Response, request, jsonify, make_response -from werkzeug.exceptions import NotFound, BadHost - -from metadata_service.api.metadata_api import metadata_api -from metadata_service.api.observability import observability +from fastapi.responses import JSONResponse +from fastapi import FastAPI +from starlette.exceptions import HTTPException +from metadata_service.api.metadata_api import metadata_router +from metadata_service.api.observability import observability_router from metadata_service.config.logging import setup_logging +from metadata_service.config.uvicorn import setup_uvicorn_logging from metadata_service.exceptions.exceptions import ( DataNotFoundException, InvalidStorageFormatException, @@ -17,99 +17,83 @@ logger = logging.getLogger() -app = Flask(__name__) -app.register_blueprint(observability) -app.register_blueprint(metadata_api) +app = FastAPI() +app.include_router(observability_router) +app.include_router(metadata_router) setup_logging(app) +setup_uvicorn_logging() -@app.after_request -def after_request(response: Response): - if ( - "Accept" in request.headers - and request.headers["Accept"] == "application/x-msgpack" - ): - # create a new Response to send the payload only as "data" field - response_msgpack = make_response(msgpack.dumps(response.json)) - response_msgpack.headers.set("Content-Type", "application/x-msgpack") - return response_msgpack - +@app.middleware("http") +async def add_language_header(request, call_next): + response = await call_next(request) + response.headers.setdefault("Content-Language", "no") return response -@app.errorhandler(Exception) -def handle_generic_exception(exc): - logger.exception(exc) - return ( - jsonify( - { - "code": 202, - "message": f"Error: {str(exc)}", - "service": "metadata-service", - "type": "SYSTEM_ERROR", - } - ), - 500, - ) - - -@app.errorhandler(NotFound) -def handle_url_invalid(exc): - logger.warning(exc, exc_info=True) - return ( - jsonify( - { +@app.exception_handler(HTTPException) +async def custom_http_exception_handler(_req, exc): + if exc.status_code == 404: + return JSONResponse( + content={ "code": 103, "message": f"Error: {str(exc)}", "service": "metadata-service", "type": "PATH_NOT_FOUND", - } - ), - 400, + }, + status_code=400, + ) + return JSONResponse( + content={ + "code": 202, + "message": f"Error: {str(exc)}", + "service": "metadata-service", + "type": "SYSTEM_ERROR", + }, + status_code=500, ) -@app.errorhandler(BadHost) -def handle_bad_host(exc): - logger.warning(exc, exc_info=True) - return ( - jsonify( - { - "code": 103, - "message": f"Error: {str(exc)}", - "service": "metadata-service", - "type": "PATH_NOT_FOUND", - } - ), - 400, +@app.exception_handler(Exception) +def handle_generic_exception(_req, exc): + logger.exception(exc) + return JSONResponse( + content={ + "code": 202, + "message": f"Error: {str(exc)}", + "service": "metadata-service", + "type": "SYSTEM_ERROR", + }, + status_code=500, ) -@app.errorhandler(DataNotFoundException) -def handle_data_not_found(exc): +@app.exception_handler(DataNotFoundException) +def handle_data_not_found(_req, exc): logger.warning(exc, exc_info=True) - return jsonify(exc.to_dict()), 404 + return JSONResponse(content=exc.to_dict(), status_code=404) -@app.errorhandler(InvalidDraftVersionException) -def handle_invalid_draft(exc): +@app.exception_handler(InvalidDraftVersionException) +def handle_invalid_draft(_req, exc): logger.warning(exc, exc_info=True) - return str(exc), 404 + return JSONResponse(content={"message": str(exc)}, status_code=404) -@app.errorhandler(RequestValidationException) -def handle_invalid_request(exc): +@app.exception_handler(RequestValidationException) +def handle_invalid_request(_req, exc): logger.warning(exc, exc_info=True) - return jsonify(exc.to_dict()), 400 + return JSONResponse(content=exc.to_dict(), status_code=400) -@app.errorhandler(InvalidStorageFormatException) -def handle_invalid_format(exc): +@app.exception_handler(InvalidStorageFormatException) +def handle_invalid_format(_req, exc): logger.exception(exc) - return jsonify(exc.to_dict()), 500 + return JSONResponse(content=exc.to_dict(), status_code=500) -# this is needed to run the application in IDE if __name__ == "__main__": - app.run(port=8000, host="0.0.0.0") + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/metadata_service/config/logging.py b/metadata_service/config/logging.py index d37b946..61c6711 100644 --- a/metadata_service/config/logging.py +++ b/metadata_service/config/logging.py @@ -5,12 +5,23 @@ import logging import datetime from time import perf_counter_ns +from typing import Callable -from flask import request, g, has_request_context +from fastapi import Request +from contextvars import ContextVar from metadata_service.config import environment +request_start_time: ContextVar[int] = ContextVar("request_start_time") +correlation_id: ContextVar[str] = ContextVar("correlation_id") +method: ContextVar[str] = ContextVar("method") +url: ContextVar[str] = ContextVar("url") +remote_host: ContextVar[str] = ContextVar("remote_host") +response_status: ContextVar[int] = ContextVar("response_status") +response_time_ms: ContextVar[int] = ContextVar("response_time_ms") + + class MicrodataJSONFormatter(logging.Formatter): def __init__(self): self.host = environment.get("DOCKER_HOST_NAME") @@ -18,11 +29,9 @@ def __init__(self): self.commit_id = environment.get("COMMIT_ID") def format(self, record: logging.LogRecord) -> str: - flask_context = _get_flask_context() stack_trace = "" if record.exc_info is not None: stack_trace = self.formatException(record.exc_info) - return json.dumps( { "@timestamp": datetime.datetime.fromtimestamp( @@ -37,77 +46,53 @@ def format(self, record: logging.LogRecord) -> str: "level": record.levelno, "levelName": record.levelname, "loggerName": record.name, - "method": flask_context["request_method"], - "responseTime": flask_context["response_time_ms"], + "method": method.get(""), + "responseTime": response_time_ms.get(""), "schemaVersion": "v3", "serviceName": "metadata-service", "serviceVersion": self.commit_id, - "source_host": flask_context["request_remote_addr"], - "statusCode": flask_context["response_status"], + "source_host": remote_host.get(""), + "statusCode": response_status.get(""), "thread": record.threadName, - "url": flask_context["request_url"], - "xRequestId": re.sub( - r"[^\w\-]", "", flask_context["x_request_id"] - ), + "url": url.get(""), + "xRequestId": re.sub(r"[^\w\-]", "", correlation_id.get("")), } ) -def _get_flask_context(): - # Logging should not fail outside of flask context - flask_context = { - "response_time_ms": "", - "response_status": "", - "x_request_id": "", - "request_method": "", - "request_remote_addr": "", - "request_url": "", - } - if not has_request_context(): - return flask_context - try: - flask_context["response_time_ms"] = getattr(g, "response_time_ms") - flask_context["response_status"] = getattr(g, "response_status") - flask_context["x_request_id"] = getattr(g, "correlation_id") - except AttributeError: - ... - try: - flask_context["request_method"] = request.method - flask_context["request_remote_addr"] = request.remote_addr - flask_context["request_url"] = request.url - except AttributeError: - ... - return flask_context - - -def setup_logging(app, log_level: int = logging.INFO) -> None: +def setup_logging(app, log_level=logging.INFO): logger = logging.getLogger() logger.setLevel(log_level) + formatter = MicrodataJSONFormatter() - stream_handler = logging.StreamHandler(sys.stdout) + + stream_handler = logging.StreamHandler() stream_handler.setFormatter(formatter) logger.addHandler(stream_handler) - @app.before_request - def before_request(): - g.response_time_ms = 0 - g.response_status = "" - g.start_time = perf_counter_ns() - correlation_id = request.headers.get("X-Request-ID", None) - if correlation_id is None: - g.correlation_id = "metadata-service-" + str(uuid.uuid1()) + @app.middleware("http") + async def add_process_time_header(request: Request, call_next: Callable): + request_start_time.set(perf_counter_ns()) + corr_id = request.headers.get("X-Request-ID", None) + if corr_id is None: + correlation_id.set("metadata-service-" + str(uuid.uuid1())) else: - g.correlation_id = correlation_id - g.method = request.method - g.url = request.url - g.remote_host = request.remote_addr + correlation_id.set(corr_id) + method.set(request.method) + url.set(str(request.url)) + client = request.client + host = "" + if client is not None: + host = client.host + remote_host.set(host) + + response = await call_next(request) - @app.after_request - def after_request(response): - g.response_time_ms = int( - (perf_counter_ns() - g.start_time) / 1_000_000 + response_time = int( + (perf_counter_ns() - request_start_time.get()) / 1_000_000 ) - g.response_status = response.status_code - response.headers["X-Request-ID"] = g.correlation_id + response_time_ms.set(response_time) + response_status.set(response.status_code) + response.headers["X-Request-ID"] = correlation_id.get() logger.info("responded") return response diff --git a/metadata_service/config/uvicorn.py b/metadata_service/config/uvicorn.py new file mode 100644 index 0000000..21db2ef --- /dev/null +++ b/metadata_service/config/uvicorn.py @@ -0,0 +1,20 @@ +from uvicorn.config import LOGGING_CONFIG + + +def setup_uvicorn_logging(): + fmt = ( + '{"@timestamp": "%(asctime)s",' + '"pid": "%(process)d", ' + '"loggerName": "%(name)s",' + '"levelName": "%(levelname)s",' + '"schemaVersion": "v3",' + '"serviceVersion": "TODO",' + '"serviceName": "metadata-service",' + '"xRequestId": "",' + '"message": "%(message)s"}' + ) + datefmt = "%Y-%m-%dT%H:%M:%S" + LOGGING_CONFIG["formatters"]["default"]["fmt"] = fmt + LOGGING_CONFIG["formatters"]["default"]["datefmt"] = datefmt + LOGGING_CONFIG["formatters"]["access"]["fmt"] = fmt + LOGGING_CONFIG["formatters"]["access"]["datefmt"] = datefmt diff --git a/poetry.lock b/poetry.lock index c5b6219..cbba9a8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -13,15 +13,36 @@ files = [ ] [[package]] -name = "blinker" -version = "1.9.0" -description = "Fast, simple object-to-object and broadcast signaling" +name = "anyio" +version = "4.9.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] +files = [ + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "certifi" +version = "2025.6.15" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, - {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, + {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, + {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, ] [[package]] @@ -133,28 +154,25 @@ files = [ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] -name = "flask" -version = "3.1.1" -description = "A simple framework for building complex web applications." +name = "fastapi" +version = "0.115.13" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c"}, - {file = "flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e"}, + {file = "fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865"}, + {file = "fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307"}, ] [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" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.47.0" +typing-extensions = ">=4.8.0" [package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "gunicorn" @@ -179,190 +197,89 @@ testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] [[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] [[package]] -name = "itsdangerous" -version = "2.2.0" -description = "Safely pass data to untrusted environments and back." +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ - {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, - {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, ] +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + [[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." optional = false -python-versions = ">=3.7" -groups = ["main"] +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] -MarkupSafe = ">=2.0" +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" [package.extras] -i18n = ["Babel (>=2.7)"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.9" -groups = ["main"] +python-versions = ">=3.6" +groups = ["main", "dev"] 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"}, + {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 = "msgpack" -version = "1.1.0" -description = "MessagePack serializer" +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, - {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, - {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, - {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, - {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, - {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, - {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, - {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, - {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, - {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, - {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, - {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, - {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, - {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -644,6 +561,36 @@ files = [ {file = "ruff-0.11.0.tar.gz", hash = "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.46.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + [[package]] name = "typing-extensions" version = "4.13.2" @@ -672,24 +619,25 @@ files = [ typing-extensions = ">=4.12.0" [[package]] -name = "werkzeug" -version = "3.1.3" -description = "The comprehensive WSGI web application library." +name = "uvicorn" +version = "0.34.3" +description = "The lightning-fast ASGI server." 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"}, + {file = "uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885"}, + {file = "uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a"}, ] [package.dependencies] -MarkupSafe = ">=2.1.1" +click = ">=7.0" +h11 = ">=0.8" [package.extras] -watchdog = ["watchdog (>=2.3)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "0c8b8e6facb3028a7dc76af6752bdbb811ced9a862e2a58c435a18567eef69b9" +content-hash = "0363ed50291341cca023429c4bee8791d043f49c90046af613f2aaa66a5bc94e" diff --git a/pyproject.toml b/pyproject.toml index 457214f..2c0171e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,10 +6,10 @@ authors = ["Microdata Developers "] [tool.poetry.dependencies] python = "^3.13" -Flask = "^3.1.1" gunicorn = "^23.0.0" pydantic = "^2.10.0" -msgpack = "^1.0.4" +fastapi = "^0.115.13" +uvicorn = "^0.34.3" [tool.poetry.group.dev.dependencies] pytest = "^8.0.1" @@ -17,6 +17,7 @@ pytest-cov = "^6.0.0" pytest-mock = "^3.7.0" pytest-dotenv = "^0.5.2" ruff = "0.11.0" +httpx = "^0.28.1" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/unit/api/test_api.py b/tests/unit/api/test_api.py index c427e93..1a5bd93 100644 --- a/tests/unit/api/test_api.py +++ b/tests/unit/api/test_api.py @@ -1,15 +1,25 @@ -from flask import Response, url_for +from fastapi import testclient -def test_client_sends_x_request_id(flask_app): - response: Response = flask_app.get( - url_for("observability.alive"), headers={"X-Request-ID": "abc123"} +def test_client_sends_x_request_id(test_app: testclient.TestClient): + response = test_app.get( + "/health/alive", headers={"X-Request-ID": "abc123"} ) assert response.status_code == 200 assert response.headers["X-Request-ID"] == "abc123" -def test_client_does_not_send_x_request_id(flask_app): - response: Response = flask_app.get(url_for("observability.alive")) +def test_client_does_not_send_x_request_id(test_app: testclient.TestClient): + response = test_app.get("/health/alive") assert response.status_code == 200 assert response.headers["X-Request-ID"] + + +def test_not_found(test_app: testclient.TestClient): + response = test_app.get("/no/such/path") + assert response.status_code == 400 + assert response.json()["code"] == 103 + assert response.json()["service"] == "metadata-service" + assert response.json()["type"] == "PATH_NOT_FOUND" + assert "Error: " in response.json()["message"] + assert response.headers["X-Request-ID"] diff --git a/tests/unit/api/test_metadata_api.py b/tests/unit/api/test_metadata_api.py index 751d958..9866a4a 100644 --- a/tests/unit/api/test_metadata_api.py +++ b/tests/unit/api/test_metadata_api.py @@ -1,8 +1,7 @@ import json -import msgpack -from flask import url_for, Response - +from fastapi import testclient +from httpx import Response from metadata_service.domain import metadata from metadata_service.domain.version import get_version_from_string @@ -64,14 +63,14 @@ METADATA_ALL_FILE_PATH = "tests/resources/fixtures/domain/metadata_all.json" -def test_get_data_store(flask_app, mocker): +def test_get_data_store(test_app: testclient.TestClient, mocker): spy = mocker.patch.object( metadata, "find_all_datastore_versions", return_value=MOCKED_DATASTORE_VERSIONS, ) - response: Response = flask_app.get( - url_for("metadata_api.get_data_store"), + response: Response = test_app.get( + "/metadata/data-store", headers={ "X-Request-ID": "test-123", "Accept-Language": "no", @@ -80,20 +79,19 @@ def test_get_data_store(flask_app, mocker): ) spy.assert_called() assert response.headers["Content-Type"] == "application/json" - assert response.json == MOCKED_DATASTORE_VERSIONS + assert response.json() == MOCKED_DATASTORE_VERSIONS -def test_get_current_data_structure_status(flask_app, mocker): +def test_get_current_data_structure_status( + test_app: testclient.TestClient, mocker +): spy = mocker.patch.object( metadata, "find_current_data_structure_status", return_value=MOCKED_DATASTRUCTURE, ) - response: Response = flask_app.get( - url_for( - "metadata_api.get_data_structure_current_status", - names="INNTEKT_TJENPEN", - ), + response: Response = test_app.get( + "metadata/data-structures/status?names=INNTEKT_TJENPEN", headers={ "X-Request-ID": "test-123", "Accept-Language": "no", @@ -102,17 +100,19 @@ def test_get_current_data_structure_status(flask_app, mocker): ) spy.assert_called_with([MOCKED_DATASTRUCTURE["name"]]) assert response.headers["Content-Type"] == "application/json" - assert response.json == MOCKED_DATASTRUCTURE + assert response.json() == MOCKED_DATASTRUCTURE -def test_get_current_data_structure_status_as_post(flask_app, mocker): +def test_get_current_data_structure_status_as_post( + test_app: testclient.TestClient, mocker +): spy = mocker.patch.object( metadata, "find_current_data_structure_status", return_value=MOCKED_DATASTRUCTURES, ) - response: Response = flask_app.post( - url_for("metadata_api.get_data_structure_current_status_as_post"), + response: Response = test_app.post( + "metadata/data-structures/status", json={"names": ",".join(list(MOCKED_DATASTRUCTURES.keys()))}, headers={ "X-Request-ID": "test-123", @@ -122,20 +122,19 @@ def test_get_current_data_structure_status_as_post(flask_app, mocker): ) spy.assert_called_with(list(MOCKED_DATASTRUCTURES.keys())) assert response.headers["Content-Type"] == "application/json" - assert response.json == MOCKED_DATASTRUCTURES + assert response.json() == MOCKED_DATASTRUCTURES -def test_get_multiple_data_structure_status(flask_app, mocker): +def test_get_multiple_data_structure_status( + test_app: testclient.TestClient, mocker +): spy = mocker.patch.object( metadata, "find_current_data_structure_status", return_value=MOCKED_DATASTRUCTURE, ) - response: Response = flask_app.get( - url_for( - "metadata_api.get_data_structure_current_status", - names="INNTEKT_TJENPEN,INNTEKT_TO", - ), + response: Response = test_app.get( + "/metadata/data-structures/status?names=INNTEKT_TJENPEN,INNTEKT_TO", headers={ "X-Request-ID": "test-123", "Accept-Language": "no", @@ -144,22 +143,18 @@ def test_get_multiple_data_structure_status(flask_app, mocker): ) spy.assert_called_with(["INNTEKT_TJENPEN", "INNTEKT_TO"]) assert response.headers["Content-Type"] == "application/json" - assert response.json == MOCKED_DATASTRUCTURE + assert response.json() == MOCKED_DATASTRUCTURE -def test_get_data_structures(flask_app, mocker): +def test_get_data_structures(test_app: testclient.TestClient, mocker): with open(DATA_STRUCTURES_FILE_PATH, encoding="utf-8") as f: mocked_data_structures = json.load(f) spy = mocker.patch.object( metadata, "find_data_structures", return_value=mocked_data_structures ) - response: Response = flask_app.get( - url_for( - "metadata_api.get_data_structures", - names="FNR,AKT_ARBAP", - version="3.2.1.0", - ), + response: Response = test_app.get( + "/metadata/data-structures?names=FNR,AKT_ARBAP&version=3.2.1.0", headers={ "X-Request-ID": "test-123", "Accept-Language": "no", @@ -171,44 +166,18 @@ def test_get_data_structures(flask_app, mocker): ["FNR", "AKT_ARBAP"], get_version_from_string("3.2.1.0"), True, False ) assert response.headers["Content-Type"] == "application/json" - assert response.json == mocked_data_structures - - -def test_get_data_structures_with_messagepack(flask_app, mocker): - with open(DATA_STRUCTURES_FILE_PATH, encoding="utf-8") as f: - mocked_data_structures = json.load(f) - - spy = mocker.patch.object( - metadata, "find_data_structures", return_value=mocked_data_structures - ) - response: Response = flask_app.get( - url_for( - "metadata_api.get_data_structures", - names="FNR,AKT_ARBAP", - version="3.2.1.0", - ), - headers={ - "X-Request-ID": "test-123", - "Accept-Language": "no", - "Accept": "application/x-msgpack", - }, - ) - spy.assert_called_with( - ["FNR", "AKT_ARBAP"], get_version_from_string("3.2.1.0"), True, False - ) - assert response.headers["Content-Type"] == "application/x-msgpack" - assert msgpack.loads(response.data) == mocked_data_structures + assert response.json() == mocked_data_structures -def test_get_all_data_structures_ever(flask_app, mocker): +def test_get_all_data_structures_ever(test_app: testclient.TestClient, mocker): mocked_data_structures = ["TEST_PERSON_INCOME", "TEST_PERSON_PETS"] spy = mocker.patch.object( metadata, "find_all_data_structures_ever", return_value=mocked_data_structures, ) - response: Response = flask_app.get( - url_for("metadata_api.get_all_data_structures_ever"), + response: Response = test_app.get( + "/metadata/all-data-structures", headers={ "X-Request-ID": "test-123", "Accept-Language": "no", @@ -218,10 +187,10 @@ def test_get_all_data_structures_ever(flask_app, mocker): spy.assert_called() assert response.headers["Content-Type"] == "application/json" - assert response.json == mocked_data_structures + assert response.json() == mocked_data_structures -def test_get_all_metadata(flask_app, mocker): +def test_get_all_metadata(test_app: testclient.TestClient, mocker): with open(DATA_STRUCTURES_FILE_PATH, encoding="utf-8") as f: mocked_data_structures = json.load(f) mocked_metadata_all = { @@ -238,8 +207,8 @@ def test_get_all_metadata(flask_app, mocker): spy = mocker.patch.object( metadata, "find_all_metadata", return_value=mocked_metadata_all ) - response: Response = flask_app.get( - url_for("metadata_api.get_all_metadata", version="3.2.1.0"), + response: Response = test_app.get( + "/metadata/all?version=3.2.1.0", headers={ "X-Request-ID": "test-123", "Accept-Language": "no", @@ -248,10 +217,12 @@ def test_get_all_metadata(flask_app, mocker): ) spy.assert_called_with(get_version_from_string("3.2.1.0"), False) assert response.headers["Content-Type"] == "application/json" - assert response.json == mocked_metadata_all + assert response.json() == mocked_metadata_all -def test_get_all_metadata_long_version_numbers(flask_app, mocker): +def test_get_all_metadata_long_version_numbers( + test_app: testclient.TestClient, mocker +): with open(DATA_STRUCTURES_FILE_PATH, encoding="utf-8") as f: mocked_data_structures = json.load(f) mocked_metadata_all = { @@ -268,8 +239,8 @@ def test_get_all_metadata_long_version_numbers(flask_app, mocker): spy = mocker.patch.object( metadata, "find_all_metadata", return_value=mocked_metadata_all ) - response: Response = flask_app.get( - url_for("metadata_api.get_all_metadata", version="1234.5678.9012.0"), + response: Response = test_app.get( + "/metadata/all?version=1234.5678.9012.0", headers={ "X-Request-ID": "test-123", "Accept-Language": "no", @@ -278,35 +249,33 @@ def test_get_all_metadata_long_version_numbers(flask_app, mocker): ) spy.assert_called_with(get_version_from_string("1234.5678.9012.0"), False) assert response.headers["Content-Type"] == "application/json" - assert response.json == mocked_metadata_all + assert response.json() == mocked_metadata_all -def test_get_languages(flask_app, mocker): +def test_get_languages(test_app: testclient.TestClient, mocker): spy = mocker.patch.object( metadata, "find_languages", return_value=MOCKED_LANGUAGES ) - response: Response = flask_app.get( - url_for("metadata_api.get_languages"), + response: Response = test_app.get( + "/languages", headers={"X-Request-ID": "test-123"}, ) spy.assert_called() assert response.headers["Content-Type"] == "application/json" - assert response.json == MOCKED_LANGUAGES + assert response.json() == MOCKED_LANGUAGES -def test_get_all_metadata_skip_code_lists(flask_app, mocker): +def test_get_all_metadata_skip_code_lists( + test_app: testclient.TestClient, mocker +): with open(METADATA_ALL_FILE_PATH, encoding="utf-8") as f: mocked_metadata_all = json.load(f) spy = mocker.patch.object( metadata, "find_all_metadata", return_value=mocked_metadata_all ) - response: Response = flask_app.get( - url_for( - "metadata_api.get_all_metadata", - version="3.2.1.0", - skip_code_lists=True, - ), + response: Response = test_app.get( + "/metadata/all?version=3.2.1.0&skip_code_lists=true", headers={ "X-Request-ID": "test-123", "Accept-Language": "no", @@ -315,23 +284,20 @@ def test_get_all_metadata_skip_code_lists(flask_app, mocker): ) spy.assert_called_with(get_version_from_string("3.2.1.0"), True) assert response.headers["Content-Type"] == "application/json" - assert response.json == mocked_metadata_all + assert response.json() == mocked_metadata_all -def test_get_data_structures_skip_code_lists(flask_app, mocker): +def test_get_data_structures_skip_code_lists( + test_app: testclient.TestClient, mocker +): with open(DATA_STRUCTURES_FILE_PATH, encoding="utf-8") as f: mocked_data_structures = json.load(f) spy = mocker.patch.object( metadata, "find_data_structures", return_value=mocked_data_structures ) - response: Response = flask_app.get( - url_for( - "metadata_api.get_data_structures", - names="FNR,AKT_ARBAP", - version="3.2.1.0", - skip_code_lists=True, - ), + response: Response = test_app.get( + "/metadata/data-structures?names=FNR,AKT_ARBAP&version=3.2.1.0&skip_code_lists=true", headers={ "X-Request-ID": "test-123", "Accept-Language": "no", @@ -342,4 +308,4 @@ def test_get_data_structures_skip_code_lists(flask_app, mocker): ["FNR", "AKT_ARBAP"], get_version_from_string("3.2.1.0"), True, True ) assert response.headers["Content-Type"] == "application/json" - assert response.json == mocked_data_structures + assert response.json() == mocked_data_structures diff --git a/tests/unit/api/test_request_models.py b/tests/unit/api/test_request_models.py index 299f133..b289684 100644 --- a/tests/unit/api/test_request_models.py +++ b/tests/unit/api/test_request_models.py @@ -39,19 +39,7 @@ def test_metadata_query_draft_version(): assert query.version == "0.0.0.12345" -def test_metadata_query_names_as_str(): - query = MetadataQuery(names="a,b", version="1.0.0.0") - assert isinstance(query.names, list) - - -def test_metadata_query_names_as_list(): - query = MetadataQuery(names=["a", "b"], version="1.0.0.0") - assert isinstance(query.names, list) - - def test_metadata_query_invalid_names(): - with pytest.raises(RequestValidationException) as e: - MetadataQuery(names={"a"}, version="1.0.0.0") - assert ( - "names field must be a list or a string" in e.value.message["message"] - ) + with pytest.raises(ValidationError) as e: + MetadataQuery(names={"a": "a"}, version="1.0.0.0") + assert "Input should be a valid string" in str(e) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f051aaf..866b2fa 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,13 +1,9 @@ import pytest -# Set up test application for all tests in session from metadata_service.app import app +from fastapi import testclient @pytest.fixture(scope="session") -def flask_app(): - client = app.test_client() - ctx = app.test_request_context() - ctx.push() - yield client - ctx.pop() +def test_app(): + yield testclient.TestClient(app)