Skip to content

Commit 559b20f

Browse files
committed
Preliminary implementation of the API and SDK.
1 parent f3eef38 commit 559b20f

36 files changed

+1722
-1
lines changed

.gitignore

+10-1
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,13 @@ coverage.xml
5050
*.pot
5151

5252
# Sphinx documentation
53-
docs/_build/
53+
docs/build/
54+
*.rst
55+
!index.rst
56+
57+
# the actual config file [HAS TO BE ALWAYS EXCLUDED!]
58+
config.yaml
59+
config.yml
60+
61+
# temp log and storage for the test database
62+
__temp*

.readthedocs.yaml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# .readthedocs.yaml
2+
# Read the Docs configuration file
3+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4+
5+
# Required
6+
version: 2
7+
8+
# Set the OS, Python version and other tools you might need
9+
build:
10+
os: ubuntu-22.04
11+
tools:
12+
python: "3.12"
13+
14+
# Build documentation in the "docs/" directory with Sphinx
15+
sphinx:
16+
configuration: docs/source/conf.py
17+
18+
# Optional but recommended, declare the Python requirements required
19+
# to build your documentation
20+
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
21+
# python:
22+
# install:
23+
# - requirements: docs/requirements.txt

trolldb/__init__.py __init__.py

File renamed without changes.

docs/Makefile

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Minimal makefile for Sphinx documentation
2+
#
3+
4+
# You can set these variables from the command line, and also
5+
# from the environment for the first two.
6+
SPHINXOPTS ?=
7+
SPHINXBUILD ?= sphinx-build
8+
SOURCEDIR = source
9+
BUILDDIR = build
10+
11+
# Put it first so that "make" without argument is like "make help".
12+
help:
13+
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14+
15+
.PHONY: help Makefile
16+
17+
# Catch-all target: route all unknown targets to Sphinx using the new
18+
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19+
%: Makefile
20+
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

docs/source/conf.py

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Configuration file for the Sphinx documentation builder.
2+
#
3+
# This file only contains a selection of the most common options. For a full
4+
# list see the documentation:
5+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
6+
7+
# -- Path setup --------------------------------------------------------------
8+
9+
# If extensions (or modules to document with autodoc) are in another directory,
10+
# add these directories to sys.path here. If the directory is relative to the
11+
# documentation root, use os.path.abspath to make it absolute, like shown here.
12+
#
13+
import os
14+
import sys
15+
16+
sys.path.insert(0, os.path.abspath('../../trolldb'))
17+
18+
# -- Project information -----------------------------------------------------
19+
20+
project = 'Pytroll-db'
21+
copyright = '2024, Pytroll'
22+
author = 'Pouria Khalaj'
23+
24+
# The full version, including alpha/beta/rc tags
25+
release = '0.1'
26+
27+
# -- General configuration ---------------------------------------------------
28+
29+
# Add any Sphinx extension module names here, as strings. They can be
30+
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
31+
# ones.
32+
extensions = [
33+
'sphinx.ext.autodoc',
34+
'sphinx.ext.viewcode',
35+
'sphinx.ext.napoleon',
36+
]
37+
# Add any paths that contain templates here, relative to this directory.
38+
templates_path = ['_templates']
39+
40+
# List of patterns, relative to source directory, that match files and
41+
# directories to ignore when looking for source files.
42+
# This pattern also affects html_static_path and html_extra_path.
43+
exclude_patterns = ["*test*"]
44+
45+
# -- Options for HTML output -------------------------------------------------
46+
47+
# The theme to use for HTML and HTML Help pages. See the documentation for
48+
# a list of builtin themes.
49+
#
50+
51+
52+
# Add any paths that contain custom static files (such as style sheets) here,
53+
# relative to this directory. They are copied after the builtin static files,
54+
# so a file named "default.css" will overwrite the builtin "default.css".
55+
# html_static_path = ['_static']
56+
57+
autodoc_default_options = {
58+
'member-order': 'bysource',
59+
'special-members': '__init__',
60+
'undoc-members': True,
61+
'exclude-members': '__weakref__'
62+
}
63+
64+
root_doc = "index"

docs/source/index.rst

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Welcome to Pytroll documentation!
2+
===========================================
3+
4+
.. toctree::
5+
:maxdepth: 2
6+
:caption: Contents:
7+
8+
modules
9+
10+
11+
12+
Indices and tables
13+
==================
14+
15+
* :ref:`genindex`
16+
* :ref:`modindex`
17+
* :ref:`search`

trolldb/api/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
Note:
3+
The following applies to the :package:`trolldb.api` and all its subpackages/modules.
4+
5+
To avoid double documentations and inconsistencies, only non-FastAPI components are documented via the docstrings.
6+
For the documentation related to the FastAPI components, check out the auto-generated documentation by FastAPI.
7+
Assuming that the API server is running on `<http://localhost:8000>`_ (example) the auto-generated documentation can
8+
be accessed via either `<http://localhost:8000/redoc>`_ or `<http://localhost:8000/docs>`_.
9+
10+
Read more at `FastAPI automatics docs <https://fastapi.tiangolo.com/features/#automatic-docs>`_.
11+
"""

trolldb/api/api.py

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
The module which includes the main functionalities of the API package. This is the main module which is supposed to be
3+
imported by the users of the package.
4+
5+
Note:
6+
Functions in this module are decorated with
7+
`pydantic.validate_call <https://docs.pydantic.dev/latest/api/validate_call/#pydantic.validate_call_decorator.validate_call>`_
8+
so that their arguments can be validated using the corresponding type hints, when calling the function at runtime.
9+
10+
Note:
11+
The following applies to the :obj:`trolldb.api` package and all its subpackages/modules.
12+
13+
To avoid redundant documentation and inconsistencies, only non-FastAPI components are documented via the docstrings.
14+
For the documentation related to the FastAPI components, check out the auto-generated documentation by FastAPI.
15+
Assuming that the API server is running on `<http://localhost:8000>`_ (example) the auto-generated documentation can
16+
be accessed via either `<http://localhost:8000/redoc>`_ or `<http://localhost:8000/docs>`_.
17+
18+
Read more at `FastAPI automatics docs <https://fastapi.tiangolo.com/features/#automatic-docs>`_.
19+
"""
20+
21+
import asyncio
22+
import time
23+
from contextlib import contextmanager
24+
from multiprocessing import Process
25+
26+
import uvicorn
27+
from fastapi import FastAPI
28+
from pydantic import FilePath, validate_call
29+
30+
from api.routes import api_router
31+
from config.config import AppConfig, parse, Timeout
32+
from database.mongodb import mongodb_context
33+
34+
35+
@validate_call
36+
def run_server(config: AppConfig | FilePath, **kwargs) -> None:
37+
"""
38+
Runs the API server with all the routes and connection to the database. It first creates a FastAPI
39+
application and runs it using `uvicorn <https://www.uvicorn.org/>`_ which is
40+
ASGI (Asynchronous Server Gateway Interface) compliant. This function runs the event loop using
41+
`asyncio <https://docs.python.org/3/library/asyncio.html>`_ and does not yield!
42+
43+
Args:
44+
config:
45+
The configuration of the application which includes both the server and database configurations. In case of
46+
a :class:`FilePath`, it should be a valid path to an existing config file which will parsed as a ``.YAML``
47+
file.
48+
49+
**kwargs:
50+
The keyword arguments are the same as those accepted by the
51+
`FastAPI class <https://fastapi.tiangolo.com/reference/fastapi/#fastapi.FastAPI>`_ and are directly passed
52+
to it. These keyword arguments will be first concatenated with the configurations of the API server which
53+
are read from the ``config`` argument. The keyword arguments which are passed
54+
explicitly to the function take precedence over ``config``.
55+
"""
56+
57+
config = parse(config)
58+
app = FastAPI(**(config.api_server._asdict() | kwargs))
59+
app.include_router(api_router)
60+
61+
async def _serve():
62+
"""
63+
An auxiliary coroutine to be used in the asynchronous execution of the FastAPI application.
64+
"""
65+
async with mongodb_context(config.database):
66+
await uvicorn.Server(
67+
config=uvicorn.Config(
68+
host=config.api_server.url.host,
69+
port=config.api_server.url.port,
70+
app=app
71+
)
72+
).serve()
73+
74+
asyncio.run(_serve())
75+
76+
77+
@contextmanager
78+
@validate_call
79+
def server_process_context(config: AppConfig | FilePath, startup_time: Timeout = 2000):
80+
"""
81+
A synchronous context manager to run the API server in a separate process (non-blocking) using the
82+
`multiprocessing <https://docs.python.org/3/library/multiprocessing.html>`_ package. The main use case is envisaged
83+
to be in testing environments.
84+
85+
Args:
86+
config:
87+
Same as ``config`` argument for :func:`run_server`.
88+
89+
startup_time:
90+
The overall time that is expected for the server and the database connections to be established before
91+
actual requests can be sent to the server. For testing purposes ensure that this is sufficiently large so
92+
that the tests will not time out.
93+
"""
94+
config = parse(config)
95+
process = Process(target=run_server, args=(config,))
96+
process.start()
97+
try:
98+
time.sleep(startup_time / 1000) # `time.sleep()` expects an argument in seconds, hence the division by 1000.
99+
yield process
100+
finally:
101+
process.terminate()

trolldb/api/errors/__init__.py

Whitespace-only changes.

trolldb/api/errors/errors.py

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
The module which defines error responses that will be returned by the API.
3+
"""
4+
5+
from collections import OrderedDict
6+
from typing import Self
7+
8+
from fastapi import status
9+
from fastapi.responses import PlainTextResponse
10+
11+
12+
class FailureResponse:
13+
descriptor_delimiter = " |OR| "
14+
defaultResponseClass = PlainTextResponse
15+
16+
def __init__(self, args_dict: dict):
17+
self.__dict = OrderedDict(args_dict)
18+
19+
def __or__(self, other: Self):
20+
buff = OrderedDict(self.__dict)
21+
for key, value in other.__dict.items():
22+
buff[key] = FailureResponse.listify(buff.get(key, []))
23+
buff[key].extend(FailureResponse.listify(value))
24+
return FailureResponse(buff)
25+
26+
def __str__(self):
27+
return str(self.__dict)
28+
29+
def fastapi_response(self, status_code: int | None = None):
30+
if status_code is None and len(self.__dict) > 1:
31+
raise ValueError("In case of multiple response status codes, please provide one.")
32+
status_code, content = [(k, v) for k, v in self.__dict.items()][0]
33+
try:
34+
return FailureResponse.defaultResponseClass(
35+
content=FailureResponse.stringify(content),
36+
status_code=status_code)
37+
except KeyError:
38+
raise KeyError(f"No default response found for the given status code: {status_code}")
39+
40+
@property
41+
def fastapi_descriptor(self):
42+
return {k: {"description": FailureResponse.stringify(v)} for k, v in self.__dict.items()}
43+
44+
@staticmethod
45+
def listify(item: str | list[str]) -> list[str]:
46+
return item if isinstance(item, list) else [item]
47+
48+
@staticmethod
49+
def stringify(item: str | list[str]) -> str:
50+
return FailureResponse.descriptor_delimiter.join(FailureResponse.listify(item))
51+
52+
53+
class BaseFailureResponses:
54+
55+
@classmethod
56+
def fields(cls):
57+
return {k: v for k, v in cls.__dict__.items() if isinstance(v, FailureResponse)}
58+
59+
@classmethod
60+
def union(cls):
61+
buff = FailureResponse({})
62+
for k, v in cls.fields().items():
63+
buff |= v
64+
return buff
65+
66+
67+
class CollectionFail(BaseFailureResponses):
68+
NOT_FOUND = FailureResponse({
69+
status.HTTP_404_NOT_FOUND:
70+
"Collection name does not exist."
71+
})
72+
73+
WRONG_TYPE = FailureResponse({
74+
status.HTTP_422_UNPROCESSABLE_ENTITY:
75+
"Collection name must be either a string or None; or both database name and collection name must be None."
76+
})
77+
78+
79+
class DatabaseFail(BaseFailureResponses):
80+
NOT_FOUND = FailureResponse({
81+
status.HTTP_404_NOT_FOUND:
82+
"Database name does not exist."
83+
})
84+
85+
WRONG_TYPE = FailureResponse({
86+
status.HTTP_422_UNPROCESSABLE_ENTITY:
87+
"Database name must be either a string or None."
88+
})
89+
90+
91+
class DocumentsFail(BaseFailureResponses):
92+
NOT_FOUND = FailureResponse({
93+
status.HTTP_404_NOT_FOUND:
94+
"Could not find any document with the given object id."
95+
})
96+
97+
98+
Database_Collection_Fail = DatabaseFail | CollectionFail
99+
Database_Collection_Document_Fail = DatabaseFail | CollectionFail | DocumentsFail
100+
101+
database_collection_fail_descriptor = (
102+
DatabaseFail.union() | CollectionFail.union()
103+
).fastapi_descriptor
104+
105+
database_collection_document_fail_descriptor = (
106+
DatabaseFail.union() | CollectionFail.union() | DocumentsFail.union()
107+
).fastapi_descriptor

trolldb/api/routes/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .router import api_router
2+
3+
__all__ = ("api_router",)

0 commit comments

Comments
 (0)