Skip to content
This repository was archived by the owner on Aug 20, 2025. It is now read-only.

Commit 94babbc

Browse files
Merge pull request #68 from statisticsnorway/fastapi
Fastapi
2 parents 5bcb7e6 + cd4e632 commit 94babbc

File tree

14 files changed

+380
-518
lines changed

14 files changed

+380
-518
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ ENV PYTHONPATH "${PYTHONPATH}:/app:/app/dependencies"
5353
# Change user
5454
USER microdata
5555

56-
CMD ["/app/dependencies/bin/gunicorn", "--logger-class", "metadata_service.config.gunicorn.CustomLogger", "metadata_service.app:app", "--workers", "2", "--limit-request-line", "8190"]
56+
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"]

metadata_service/adapter/datastore.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def get_datastore_versions() -> dict:
2525
return json.load(f)
2626

2727

28-
def _get_draft_metadata_all():
28+
def _get_draft_metadata_all() -> dict:
2929
metadata_all_file_path = (
3030
f"{DATASTORE_ROOT_DIR}/datastore/metadata_all__DRAFT.json"
3131
)
@@ -34,7 +34,7 @@ def _get_draft_metadata_all():
3434

3535

3636
@lru_cache(maxsize=32)
37-
def _get_versioned_metadata_all(version: Version):
37+
def _get_versioned_metadata_all(version: Version) -> dict:
3838
file_version = version.to_3_underscored()
3939
metadata_all_file_path = (
4040
f"{DATASTORE_ROOT_DIR}/datastore/metadata_all__{file_version}.json"
@@ -43,7 +43,7 @@ def _get_versioned_metadata_all(version: Version):
4343
return json.load(f)
4444

4545

46-
def get_metadata_all(version: Version) -> str:
46+
def get_metadata_all(version: Version) -> dict:
4747
try:
4848
if version.is_draft():
4949
return _get_draft_metadata_all()
Lines changed: 32 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,73 @@
11
import logging
22

3-
from flask import Blueprint, jsonify, request
3+
from fastapi import APIRouter, Depends
44

55
from metadata_service.api.request_models import NameParam, MetadataQuery
66
from metadata_service.domain import metadata
77
from metadata_service.domain.version import get_version_from_string
88

99
logger = logging.getLogger()
10-
metadata_api = Blueprint("metadata_api", __name__)
10+
metadata_router = APIRouter()
1111

1212

13-
@metadata_api.get("/metadata/data-store")
13+
@metadata_router.get("/metadata/data-store")
1414
def get_data_store():
1515
logger.info("GET /metadata/data-store")
16+
return metadata.find_all_datastore_versions()
1617

17-
response = jsonify(metadata.find_all_datastore_versions())
18-
response.headers.set("content-language", "no")
19-
return response
2018

21-
22-
@metadata_api.get("/metadata/data-structures/status")
23-
def get_data_structure_current_status():
24-
validated_query = NameParam(**request.args)
19+
@metadata_router.get("/metadata/data-structures/status")
20+
def get_data_structure_current_status(validated_query: NameParam = Depends()):
2521
logger.info(
2622
f"GET /metadata/data-structures/status with name = {validated_query.names}"
2723
)
28-
response = jsonify(
29-
metadata.find_current_data_structure_status(
30-
validated_query.get_names_as_list()
31-
)
24+
return metadata.find_current_data_structure_status(
25+
validated_query.get_names_as_list()
3226
)
33-
response.headers.set("content-language", "no")
34-
return response
35-
3627

37-
@metadata_api.post("/metadata/data-structures/status")
38-
def get_data_structure_current_status_as_post():
39-
validated_body = NameParam(**request.json)
4028

29+
@metadata_router.post("/metadata/data-structures/status")
30+
def get_data_structure_current_status_as_post(validated_body: NameParam):
4131
logger.info(
4232
f"POST /metadata/data-structures/status with name = {validated_body.names}"
4333
)
44-
response = jsonify(
45-
metadata.find_current_data_structure_status(
46-
validated_body.get_names_as_list()
47-
)
34+
return metadata.find_current_data_structure_status(
35+
validated_body.get_names_as_list()
4836
)
49-
response.headers.set("content-language", "no")
50-
return response
5137

5238

53-
@metadata_api.get("/metadata/data-structures")
54-
def get_data_structures():
55-
validated_query = MetadataQuery(**request.args)
39+
@metadata_router.get("/metadata/data-structures")
40+
def get_data_structures(
41+
validated_query: MetadataQuery = Depends(),
42+
):
5643
validated_query.include_attributes = True
5744
logger.info(f"GET /metadata/data-structures with query: {validated_query}")
58-
59-
response = jsonify(
60-
metadata.find_data_structures(
61-
validated_query.names,
62-
get_version_from_string(validated_query.version),
63-
validated_query.include_attributes,
64-
validated_query.skip_code_lists,
65-
)
45+
return metadata.find_data_structures(
46+
validated_query.names_as_list(),
47+
get_version_from_string(validated_query.version),
48+
validated_query.include_attributes,
49+
validated_query.skip_code_lists,
6650
)
67-
response.headers.set("content-language", "no")
68-
return response
6951

7052

71-
@metadata_api.get("/metadata/all-data-structures")
53+
@metadata_router.get("/metadata/all-data-structures")
7254
def get_all_data_structures_ever():
7355
logger.info("GET /metadata/all-data-structures")
56+
return metadata.find_all_data_structures_ever()
7457

75-
response = jsonify(metadata.find_all_data_structures_ever())
76-
response.headers.set("content-language", "no")
77-
return response
7858

79-
80-
@metadata_api.get("/metadata/all")
81-
def get_all_metadata():
82-
validated_query = MetadataQuery(**request.args)
59+
@metadata_router.get("/metadata/all")
60+
def get_all_metadata(
61+
validated_query: MetadataQuery = Depends(),
62+
):
8363
logger.info(f"GET /metadata/all with version: {validated_query.version}")
84-
85-
response = jsonify(
86-
metadata.find_all_metadata(
87-
get_version_from_string(validated_query.version),
88-
validated_query.skip_code_lists,
89-
)
64+
return metadata.find_all_metadata(
65+
get_version_from_string(validated_query.version),
66+
validated_query.skip_code_lists,
9067
)
91-
response.headers.set("content-language", "no")
92-
return response
9368

9469

95-
@metadata_api.get("/languages")
70+
@metadata_router.get("/languages")
9671
def get_languages():
9772
logger.info("GET /languages")
98-
99-
response = jsonify(metadata.find_languages())
100-
return response
73+
return metadata.find_languages()
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
from flask import Blueprint
1+
from fastapi import APIRouter
22

33

4-
observability = Blueprint("observability", __name__)
4+
observability_router = APIRouter()
55

66

7-
@observability.get("/health/alive")
8-
def alive():
7+
@observability_router.get("/health/alive")
8+
async def alive():
99
return "I'm alive!"
1010

1111

12-
@observability.get("/health/ready")
13-
def ready():
12+
@observability_router.get("/health/ready")
13+
async def ready():
1414
return "I'm ready!"

metadata_service/api/request_models.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,11 @@
1111

1212

1313
class MetadataQuery(BaseModel, extra="forbid", validate_assignment=True):
14-
names: List[str] = []
14+
names: str | None = None
1515
version: str
1616
include_attributes: bool = False
1717
skip_code_lists: bool = False
1818

19-
@field_validator("names", mode="before")
20-
@classmethod
21-
def split_str(cls, names):
22-
if isinstance(names, List):
23-
return names[0].split(",")
24-
elif isinstance(names, str):
25-
return names.split(",")
26-
else:
27-
raise RequestValidationException(
28-
"names field must be a list or a string"
29-
)
30-
3119
@field_validator("version", mode="before")
3220
@classmethod
3321
def validate_version(cls, version: str):
@@ -38,6 +26,9 @@ def validate_version(cls, version: str):
3826
)
3927
return version
4028

29+
def names_as_list(self) -> List[str]:
30+
return [] if self.names is None else self.names.split(",")
31+
4132

4233
class NameParam(BaseModel, extra="forbid"):
4334
names: str

metadata_service/app.py

Lines changed: 56 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import logging
22

3-
import msgpack
4-
from flask import Flask, Response, request, jsonify, make_response
5-
from werkzeug.exceptions import NotFound, BadHost
6-
7-
from metadata_service.api.metadata_api import metadata_api
8-
from metadata_service.api.observability import observability
3+
from fastapi.responses import JSONResponse
4+
from fastapi import FastAPI
5+
from starlette.exceptions import HTTPException
6+
from metadata_service.api.metadata_api import metadata_router
7+
from metadata_service.api.observability import observability_router
98
from metadata_service.config.logging import setup_logging
9+
from metadata_service.config.uvicorn import setup_uvicorn_logging
1010
from metadata_service.exceptions.exceptions import (
1111
DataNotFoundException,
1212
InvalidStorageFormatException,
@@ -17,99 +17,83 @@
1717

1818
logger = logging.getLogger()
1919

20-
app = Flask(__name__)
21-
app.register_blueprint(observability)
22-
app.register_blueprint(metadata_api)
20+
app = FastAPI()
21+
app.include_router(observability_router)
22+
app.include_router(metadata_router)
2323

2424
setup_logging(app)
25+
setup_uvicorn_logging()
2526

2627

27-
@app.after_request
28-
def after_request(response: Response):
29-
if (
30-
"Accept" in request.headers
31-
and request.headers["Accept"] == "application/x-msgpack"
32-
):
33-
# create a new Response to send the payload only as "data" field
34-
response_msgpack = make_response(msgpack.dumps(response.json))
35-
response_msgpack.headers.set("Content-Type", "application/x-msgpack")
36-
return response_msgpack
37-
28+
@app.middleware("http")
29+
async def add_language_header(request, call_next):
30+
response = await call_next(request)
31+
response.headers.setdefault("Content-Language", "no")
3832
return response
3933

4034

41-
@app.errorhandler(Exception)
42-
def handle_generic_exception(exc):
43-
logger.exception(exc)
44-
return (
45-
jsonify(
46-
{
47-
"code": 202,
48-
"message": f"Error: {str(exc)}",
49-
"service": "metadata-service",
50-
"type": "SYSTEM_ERROR",
51-
}
52-
),
53-
500,
54-
)
55-
56-
57-
@app.errorhandler(NotFound)
58-
def handle_url_invalid(exc):
59-
logger.warning(exc, exc_info=True)
60-
return (
61-
jsonify(
62-
{
35+
@app.exception_handler(HTTPException)
36+
async def custom_http_exception_handler(_req, exc):
37+
if exc.status_code == 404:
38+
return JSONResponse(
39+
content={
6340
"code": 103,
6441
"message": f"Error: {str(exc)}",
6542
"service": "metadata-service",
6643
"type": "PATH_NOT_FOUND",
67-
}
68-
),
69-
400,
44+
},
45+
status_code=400,
46+
)
47+
return JSONResponse(
48+
content={
49+
"code": 202,
50+
"message": f"Error: {str(exc)}",
51+
"service": "metadata-service",
52+
"type": "SYSTEM_ERROR",
53+
},
54+
status_code=500,
7055
)
7156

7257

73-
@app.errorhandler(BadHost)
74-
def handle_bad_host(exc):
75-
logger.warning(exc, exc_info=True)
76-
return (
77-
jsonify(
78-
{
79-
"code": 103,
80-
"message": f"Error: {str(exc)}",
81-
"service": "metadata-service",
82-
"type": "PATH_NOT_FOUND",
83-
}
84-
),
85-
400,
58+
@app.exception_handler(Exception)
59+
def handle_generic_exception(_req, exc):
60+
logger.exception(exc)
61+
return JSONResponse(
62+
content={
63+
"code": 202,
64+
"message": f"Error: {str(exc)}",
65+
"service": "metadata-service",
66+
"type": "SYSTEM_ERROR",
67+
},
68+
status_code=500,
8669
)
8770

8871

89-
@app.errorhandler(DataNotFoundException)
90-
def handle_data_not_found(exc):
72+
@app.exception_handler(DataNotFoundException)
73+
def handle_data_not_found(_req, exc):
9174
logger.warning(exc, exc_info=True)
92-
return jsonify(exc.to_dict()), 404
75+
return JSONResponse(content=exc.to_dict(), status_code=404)
9376

9477

95-
@app.errorhandler(InvalidDraftVersionException)
96-
def handle_invalid_draft(exc):
78+
@app.exception_handler(InvalidDraftVersionException)
79+
def handle_invalid_draft(_req, exc):
9780
logger.warning(exc, exc_info=True)
98-
return str(exc), 404
81+
return JSONResponse(content={"message": str(exc)}, status_code=404)
9982

10083

101-
@app.errorhandler(RequestValidationException)
102-
def handle_invalid_request(exc):
84+
@app.exception_handler(RequestValidationException)
85+
def handle_invalid_request(_req, exc):
10386
logger.warning(exc, exc_info=True)
104-
return jsonify(exc.to_dict()), 400
87+
return JSONResponse(content=exc.to_dict(), status_code=400)
10588

10689

107-
@app.errorhandler(InvalidStorageFormatException)
108-
def handle_invalid_format(exc):
90+
@app.exception_handler(InvalidStorageFormatException)
91+
def handle_invalid_format(_req, exc):
10992
logger.exception(exc)
110-
return jsonify(exc.to_dict()), 500
93+
return JSONResponse(content=exc.to_dict(), status_code=500)
11194

11295

113-
# this is needed to run the application in IDE
11496
if __name__ == "__main__":
115-
app.run(port=8000, host="0.0.0.0")
97+
import uvicorn
98+
99+
uvicorn.run(app, host="0.0.0.0", port=8000)

0 commit comments

Comments
 (0)