tiny-service-core 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.3
2
+ Name: tiny-service-core
3
+ Version: 0.1.0
4
+ Summary: 🪐 Shared functionality for tiny microservices
5
+ Keywords: async,fastapi,microservice,core
6
+ Author: Aliaksei Urbanski
7
+ Author-email: Aliaksei Urbanski <aliaksei.urbanski@gmail.com>
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Framework :: Pydantic
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Internet
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
20
+ Classifier: Topic :: Software Development
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Classifier: Programming Language :: Python
26
+ Classifier: Programming Language :: Python :: 3.12
27
+ Classifier: Programming Language :: Python :: 3.13
28
+ Classifier: Programming Language :: Python :: 3.14
29
+ Classifier: Programming Language :: Python :: Implementation :: CPython
30
+ Requires-Dist: fastapi
31
+ Requires-Dist: pydantic-settings
32
+ Requires-Dist: sentry-sdk[fastapi]
33
+ Requires-Dist: redis ; extra == 'cache'
34
+ Requires-Dist: alembic ; extra == 'db'
35
+ Requires-Dist: asyncpg ; extra == 'db'
36
+ Requires-Dist: sentry-sdk[asyncpg,sqlalchemy] ; extra == 'db'
37
+ Requires-Dist: sqlmodel ; extra == 'db'
38
+ Requires-Dist: fastapi[standard] ; extra == 'dev'
39
+ Requires-Dist: httpx ; extra == 'http-client'
40
+ Requires-Dist: sentry-sdk[httpx] ; extra == 'http-client'
41
+ Maintainer: Aliaksei Urbanski
42
+ Maintainer-email: Aliaksei Urbanski <aliaksei.urbanski@gmail.com>
43
+ Requires-Python: >=3.12
44
+ Project-URL: Issues, https://github.com/Jamim/tiny-service-core/issues
45
+ Project-URL: Repository, https://github.com/Jamim/tiny-service-core
46
+ Provides-Extra: cache
47
+ Provides-Extra: db
48
+ Provides-Extra: dev
49
+ Provides-Extra: http-client
50
+ Description-Content-Type: text/markdown
51
+
52
+ # 🪐 Tiny Service Core
53
+
54
+ [![CI][ci-badge]][ci]
55
+ [![Coverage][cov-badge]][cov]
56
+ [![License][license-badge]][license]
57
+ [![Version][ver-badge]][pypi]
58
+ [![Python][py-badge]][pypi]
59
+
60
+ This library implements common functions that allows
61
+ you to build tiny microservices with less boilerplate.
62
+
63
+ [ci-badge]: https://img.shields.io/github/actions/workflow/status/Jamim/tiny-service-core/ci.yml.svg
64
+ [ci]: https://github.com/Jamim/tiny-service-core/actions/workflows/ci.yml
65
+ [cov-badge]: https://codecov.io/github/Jamim/tiny-service-core/graph/badge.svg
66
+ [cov]: https://app.codecov.io/github/Jamim/tiny-service-core
67
+ [license-badge]: https://img.shields.io/github/license/Jamim/tiny-service-core
68
+ [ver-badge]: https://img.shields.io/pypi/v/tiny-service-core
69
+ [pypi]: https://pypi.org/project/tiny-service-core/
70
+ [py-badge]: https://img.shields.io/pypi/pyversions/tiny-service-core
71
+ [license]: https://github.com/Jamim/tiny-service-core/blob/main/LICENSE
72
+
73
+ ## Installation
74
+
75
+ Install with `pip`, including optional dependencies:
76
+
77
+ ```shell
78
+ pip install tiny-service-core[cache,db,http-client]
79
+ ```
80
+
81
+ Clearly, you can use any other Python package manager instead.
82
+ I personally prefer using [uv][uv] nowadays.
83
+
84
+ ```shell
85
+ uv add tiny-service-core --extra cache --extra db --extra http-client
86
+ ```
87
+
88
+ [uv]: https://docs.astral.sh/uv/
89
+
90
+ ## Dependencies
91
+
92
+ Tiny Service Core is highly opinionated.
93
+ Current version glues together these components:
94
+ - [FastAPI][fastapi]
95
+ - [pydantic-settings][pydantic-settings]
96
+ - [Sentry][sentry]
97
+ - with `cache` [extra][installing-extras]
98
+ * [Redis][redis]
99
+ - `db`
100
+ * [SQLModel][sqlmodel]
101
+ * [asyncpg][asyncpg]
102
+ * [Alembic][alembic]
103
+ - `http-client`
104
+ * [HTTPX][httpx]
105
+
106
+ [fastapi]: https://fastapi.tiangolo.com
107
+ [pydantic-settings]: https://docs.pydantic.dev/latest/concepts/pydantic_settings/
108
+ [sentry]: https://docs.sentry.io/platforms/python/
109
+ [redis]: https://github.com/redis/redis-py
110
+ [sqlmodel]: https://sqlmodel.tiangolo.com
111
+ [asyncpg]: https://github.com/MagicStack/asyncpg
112
+ [alembic]: https://alembic.sqlalchemy.org
113
+ [httpx]: https://www.python-httpx.org
114
+
115
+ [installing-extras]: https://packaging.python.org/en/latest/tutorials/installing-packages/#installing-extras
@@ -0,0 +1,64 @@
1
+ # 🪐 Tiny Service Core
2
+
3
+ [![CI][ci-badge]][ci]
4
+ [![Coverage][cov-badge]][cov]
5
+ [![License][license-badge]][license]
6
+ [![Version][ver-badge]][pypi]
7
+ [![Python][py-badge]][pypi]
8
+
9
+ This library implements common functions that allows
10
+ you to build tiny microservices with less boilerplate.
11
+
12
+ [ci-badge]: https://img.shields.io/github/actions/workflow/status/Jamim/tiny-service-core/ci.yml.svg
13
+ [ci]: https://github.com/Jamim/tiny-service-core/actions/workflows/ci.yml
14
+ [cov-badge]: https://codecov.io/github/Jamim/tiny-service-core/graph/badge.svg
15
+ [cov]: https://app.codecov.io/github/Jamim/tiny-service-core
16
+ [license-badge]: https://img.shields.io/github/license/Jamim/tiny-service-core
17
+ [ver-badge]: https://img.shields.io/pypi/v/tiny-service-core
18
+ [pypi]: https://pypi.org/project/tiny-service-core/
19
+ [py-badge]: https://img.shields.io/pypi/pyversions/tiny-service-core
20
+ [license]: https://github.com/Jamim/tiny-service-core/blob/main/LICENSE
21
+
22
+ ## Installation
23
+
24
+ Install with `pip`, including optional dependencies:
25
+
26
+ ```shell
27
+ pip install tiny-service-core[cache,db,http-client]
28
+ ```
29
+
30
+ Clearly, you can use any other Python package manager instead.
31
+ I personally prefer using [uv][uv] nowadays.
32
+
33
+ ```shell
34
+ uv add tiny-service-core --extra cache --extra db --extra http-client
35
+ ```
36
+
37
+ [uv]: https://docs.astral.sh/uv/
38
+
39
+ ## Dependencies
40
+
41
+ Tiny Service Core is highly opinionated.
42
+ Current version glues together these components:
43
+ - [FastAPI][fastapi]
44
+ - [pydantic-settings][pydantic-settings]
45
+ - [Sentry][sentry]
46
+ - with `cache` [extra][installing-extras]
47
+ * [Redis][redis]
48
+ - `db`
49
+ * [SQLModel][sqlmodel]
50
+ * [asyncpg][asyncpg]
51
+ * [Alembic][alembic]
52
+ - `http-client`
53
+ * [HTTPX][httpx]
54
+
55
+ [fastapi]: https://fastapi.tiangolo.com
56
+ [pydantic-settings]: https://docs.pydantic.dev/latest/concepts/pydantic_settings/
57
+ [sentry]: https://docs.sentry.io/platforms/python/
58
+ [redis]: https://github.com/redis/redis-py
59
+ [sqlmodel]: https://sqlmodel.tiangolo.com
60
+ [asyncpg]: https://github.com/MagicStack/asyncpg
61
+ [alembic]: https://alembic.sqlalchemy.org
62
+ [httpx]: https://www.python-httpx.org
63
+
64
+ [installing-extras]: https://packaging.python.org/en/latest/tutorials/installing-packages/#installing-extras
@@ -0,0 +1,136 @@
1
+ [project]
2
+ name = "tiny-service-core"
3
+ description = "🪐 Shared functionality for tiny microservices"
4
+
5
+ # https://github.com/astral-sh/uv/issues/11718
6
+ version = "0.1.0"
7
+
8
+ readme = "README.md"
9
+ license = { text = "MIT" }
10
+
11
+ authors = [
12
+ { name = "Aliaksei Urbanski", email = "aliaksei.urbanski@gmail.com" },
13
+ ]
14
+ maintainers = [
15
+ { name = "Aliaksei Urbanski", email = "aliaksei.urbanski@gmail.com" },
16
+ ]
17
+
18
+ classifiers = [
19
+ "License :: OSI Approved :: MIT License",
20
+ "Development Status :: 4 - Beta",
21
+ "Environment :: Web Environment",
22
+ "Framework :: AsyncIO",
23
+ "Framework :: FastAPI",
24
+ "Framework :: Pydantic",
25
+ "Intended Audience :: Developers",
26
+ "Operating System :: OS Independent",
27
+ "Topic :: Internet",
28
+ "Topic :: Internet :: WWW/HTTP",
29
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
30
+ "Topic :: Software Development",
31
+ "Topic :: Software Development :: Libraries",
32
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
33
+ "Topic :: Software Development :: Libraries :: Python Modules",
34
+ "Typing :: Typed",
35
+ "Programming Language :: Python",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Programming Language :: Python :: 3.13",
38
+ "Programming Language :: Python :: 3.14",
39
+ "Programming Language :: Python :: Implementation :: CPython",
40
+ ]
41
+ keywords = ["async", "fastapi", "microservice", "core"]
42
+
43
+ requires-python = ">=3.12"
44
+ dependencies = [
45
+ "fastapi",
46
+ "pydantic-settings",
47
+ "sentry-sdk[fastapi]",
48
+ ]
49
+
50
+ [project.optional-dependencies]
51
+ dev = [
52
+ "fastapi[standard]",
53
+ ]
54
+ cache = [
55
+ "redis",
56
+ ]
57
+ db = [
58
+ "alembic",
59
+ "asyncpg",
60
+ "sentry-sdk[asyncpg,sqlalchemy]",
61
+ "sqlmodel",
62
+ ]
63
+ http-client = [
64
+ "httpx",
65
+ "sentry-sdk[httpx]",
66
+ ]
67
+
68
+ [dependency-groups]
69
+ lint = [
70
+ "mypy",
71
+ "ruff",
72
+ ]
73
+ test = [
74
+ "pytest",
75
+ "pytest-cov",
76
+ "pytest-env",
77
+ ]
78
+
79
+ [project.urls]
80
+ Repository = "https://github.com/Jamim/tiny-service-core"
81
+ Issues = "https://github.com/Jamim/tiny-service-core/issues"
82
+
83
+ [build-system]
84
+ requires = ["uv_build>=0.9.15,<0.10.0"]
85
+ build-backend = "uv_build"
86
+
87
+ [tool.uv.build-backend]
88
+ module-name = "core"
89
+
90
+ [tool.ruff]
91
+ line-length = 79
92
+
93
+ [tool.ruff.format]
94
+ quote-style = "single"
95
+
96
+ [tool.ruff.lint]
97
+ select = [
98
+ # pycodestyle
99
+ "E",
100
+ # Pyflakes
101
+ "F",
102
+ # pyupgrade
103
+ "UP",
104
+ # flake8-bugbear
105
+ "B",
106
+ # flake8-simplify
107
+ "SIM",
108
+ # isort
109
+ "I",
110
+ ]
111
+
112
+ [tool.ruff.lint.per-file-ignores]
113
+ "src/core/__init__.py" = ["F401", "E402"]
114
+ "src/core/deps/__init__.py" = ["F401"]
115
+
116
+ [tool.ruff.lint.isort]
117
+ known-local-folder = ["core"]
118
+
119
+ [tool.mypy]
120
+ mypy_path = "stubs"
121
+ files = "src/**/*.py"
122
+ plugins = "pydantic.mypy"
123
+ strict = true
124
+
125
+ [tool.pytest.ini_options]
126
+ addopts = "--cov core"
127
+
128
+ [tool.pytest_env]
129
+ APP_PROJ = "tiny-service"
130
+ APP_NAME = "core"
131
+ APP_ENV = "test"
132
+ APP_KEY = "42"
133
+ DB_URL = "postgresql+asyncpg://foo:bar@127.0.0.1/dummy"
134
+
135
+ [tool.coverage.run]
136
+ branch = true
@@ -0,0 +1,53 @@
1
+ import logging
2
+ from importlib.metadata import version
3
+ from typing import Any, cast
4
+
5
+ from fastapi import FastAPI
6
+
7
+ from .config import CoreSettings
8
+
9
+ settings_class: type[CoreSettings]
10
+ try:
11
+ from app.config import Settings
12
+ except ImportError:
13
+ settings_class = CoreSettings
14
+ else:
15
+ if issubclass(Settings, CoreSettings):
16
+ settings_class = cast(type[CoreSettings], Settings)
17
+ else:
18
+ settings_class = type('CombinedSettings', (Settings, CoreSettings), {})
19
+
20
+
21
+ settings = settings_class()
22
+ logger = logging.getLogger(settings.app_title)
23
+
24
+ from .app import make_app
25
+ from .crypto import get_key_hash
26
+
27
+
28
+ def run(app: FastAPI, **kwargs: Any) -> None:
29
+ import uvicorn
30
+
31
+ kwargs.setdefault('host', settings.app_host)
32
+ kwargs.setdefault('port', settings.app_port)
33
+
34
+ uvicorn.run(app, **kwargs)
35
+
36
+
37
+ __all__ = [
38
+ 'get_key_hash',
39
+ 'logger',
40
+ 'make_app',
41
+ 'run',
42
+ 'settings',
43
+ ]
44
+
45
+ try:
46
+ from .lock import Lock
47
+ except ImportError:
48
+ pass
49
+ else:
50
+ __all__.append('Lock')
51
+
52
+ __name__ = 'tiny-service-core'
53
+ __version__ = version(__name__)
@@ -0,0 +1,50 @@
1
+ from typing import Any, Literal
2
+
3
+ from fastapi import APIRouter, FastAPI, params
4
+
5
+ from . import settings
6
+ from .deps import VerifyClient
7
+ from .lifespan import lifespan
8
+ from .sentry import init_sentry
9
+
10
+
11
+ def make_app(**kwargs: Any) -> FastAPI:
12
+ init_sentry()
13
+
14
+ slug = settings.app_name
15
+
16
+ app = FastAPI(
17
+ lifespan=lifespan,
18
+ docs_url=f'/{slug}/docs',
19
+ redoc_url=f'/{slug}/redoc',
20
+ openapi_url=(
21
+ None
22
+ if settings.app_env == 'production'
23
+ else f'/{slug}/openapi.json'
24
+ ),
25
+ title=settings.app_title,
26
+ **kwargs,
27
+ )
28
+
29
+ add_router(app, 'public', f'/api/v1/{slug}')
30
+ add_router(app, 'internal', f'/internal/{slug}', [VerifyClient])
31
+
32
+ return app
33
+
34
+
35
+ def add_router(
36
+ app: FastAPI,
37
+ router_type: Literal['public', 'internal'],
38
+ prefix: str,
39
+ dependencies: list[params.Depends] | None = None,
40
+ ) -> None:
41
+ from app import api
42
+
43
+ func_name = f'add_{router_type}_api'
44
+ if not hasattr(api, func_name):
45
+ return
46
+
47
+ router = APIRouter(prefix=prefix, dependencies=dependencies)
48
+ getattr(api, func_name)(router)
49
+
50
+ app.include_router(router)
@@ -0,0 +1,78 @@
1
+ from typing import TYPE_CHECKING, Annotated, Literal
2
+
3
+ from pydantic import Field, field_validator
4
+ from pydantic_settings import BaseSettings, NoDecode
5
+
6
+ SLUG_PATTERN = r'^[a-z0-9-]+$'
7
+
8
+
9
+ class AppSettings(BaseSettings):
10
+ @property
11
+ def app_title(self) -> str:
12
+ return f'{self.app_proj}-{self.app_name}'
13
+
14
+ app_proj: str = Field(pattern=SLUG_PATTERN)
15
+ app_name: str = Field(pattern=SLUG_PATTERN)
16
+
17
+ app_env: Literal['production', 'staging', 'local', 'test']
18
+ app_key: str
19
+
20
+ app_host: str = '127.0.0.1'
21
+ app_port: int = 8080
22
+
23
+ app_client_key_hashes: Annotated[list[str], NoDecode] = []
24
+
25
+ @field_validator('app_client_key_hashes', mode='before')
26
+ @classmethod
27
+ def split_hashes(cls, hashes: str | list[str]) -> list[str]:
28
+ if isinstance(hashes, str):
29
+ hashes = hashes.split(',')
30
+ return hashes
31
+
32
+
33
+ class SentrySettings(BaseSettings):
34
+ sentry_dsn: str | None = None
35
+ traces_sample_rate: float = 1.0
36
+
37
+
38
+ class CacheSettings(BaseSettings):
39
+ cache_url: str = 'redis://localhost:6379'
40
+
41
+
42
+ class DBSettings(BaseSettings):
43
+ db_url: str
44
+
45
+
46
+ class HTTPClientSettings(BaseSettings):
47
+ http_retries: int = 2
48
+
49
+
50
+ OPTIONAL_SETTINGS: dict[str, type[BaseSettings]] = {
51
+ 'redis': CacheSettings,
52
+ 'sqlmodel': DBSettings,
53
+ 'httpx': HTTPClientSettings,
54
+ }
55
+
56
+ settings_bases = [AppSettings, SentrySettings]
57
+
58
+ for module, base in OPTIONAL_SETTINGS.items():
59
+ try:
60
+ __import__(module)
61
+ except ImportError:
62
+ pass
63
+ else:
64
+ settings_bases.append(base)
65
+
66
+
67
+ if TYPE_CHECKING:
68
+
69
+ class CoreSettings(
70
+ AppSettings,
71
+ SentrySettings,
72
+ CacheSettings,
73
+ DBSettings,
74
+ HTTPClientSettings,
75
+ ): ...
76
+ else:
77
+
78
+ class CoreSettings(*settings_bases): ...
@@ -0,0 +1,5 @@
1
+ from hashlib import blake2s
2
+
3
+
4
+ def get_key_hash(key: str) -> str:
5
+ return blake2s(key.encode()).hexdigest()
@@ -0,0 +1,28 @@
1
+ from .auth import AppClientKey as Token
2
+ from .auth import VerifyClient
3
+
4
+ __all__ = [
5
+ 'Token',
6
+ 'VerifyClient',
7
+ ]
8
+
9
+ try:
10
+ from .cache import Cache
11
+ except ImportError:
12
+ pass
13
+ else:
14
+ __all__.append('Cache')
15
+
16
+ try:
17
+ from .db import Session
18
+ except ImportError:
19
+ pass
20
+ else:
21
+ __all__.append('Session')
22
+
23
+ try:
24
+ from .http_client import HTTPClient
25
+ except ImportError:
26
+ pass
27
+ else:
28
+ __all__.append('HTTPClient')
@@ -0,0 +1,31 @@
1
+ import hmac
2
+ from typing import Annotated
3
+
4
+ from fastapi import Depends, HTTPException, status
5
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
6
+
7
+ import core
8
+ from ..crypto import get_key_hash
9
+
10
+
11
+ def get_app_client_key(
12
+ credentials: Annotated[
13
+ HTTPAuthorizationCredentials, Depends(HTTPBearer())
14
+ ],
15
+ ) -> str:
16
+ return credentials.credentials
17
+
18
+
19
+ AppClientKey = Annotated[str, Depends(get_app_client_key)]
20
+
21
+
22
+ def verify_app_client_key(app_key: AppClientKey) -> None:
23
+ key_hash = get_key_hash(app_key)
24
+ for allowed_key_hash in core.settings.app_client_key_hashes:
25
+ if hmac.compare_digest(key_hash, allowed_key_hash):
26
+ return
27
+
28
+ raise HTTPException(status.HTTP_401_UNAUTHORIZED, 'App key is not valid')
29
+
30
+
31
+ VerifyClient = Depends(verify_app_client_key)
@@ -0,0 +1,11 @@
1
+ from typing import Annotated, cast
2
+
3
+ from fastapi import Depends, Request
4
+ from redis.asyncio.client import Redis
5
+
6
+
7
+ def get_cache(request: Request) -> Redis:
8
+ return cast(Redis, request.app.state.cache)
9
+
10
+
11
+ Cache = Annotated[Redis, Depends(get_cache)]
@@ -0,0 +1,20 @@
1
+ from collections.abc import AsyncGenerator
2
+ from typing import Annotated, cast
3
+
4
+ from fastapi import Depends, Request
5
+ from sqlalchemy.orm import sessionmaker
6
+ from sqlmodel.ext.asyncio.session import AsyncSession
7
+
8
+
9
+ async def get_session(request: Request) -> AsyncGenerator[AsyncSession]:
10
+ async_session = sessionmaker(
11
+ request.app.state.db,
12
+ class_=AsyncSession,
13
+ expire_on_commit=False,
14
+ )
15
+ session = cast(AsyncSession, async_session())
16
+ async with session:
17
+ yield session
18
+
19
+
20
+ Session = Annotated[AsyncSession, Depends(get_session)]
@@ -0,0 +1,11 @@
1
+ from typing import Annotated, cast
2
+
3
+ from fastapi import Depends, Request
4
+ from httpx import AsyncClient
5
+
6
+
7
+ def get_http_client(request: Request) -> AsyncClient:
8
+ return cast(AsyncClient, request.app.state.http_client)
9
+
10
+
11
+ HTTPClient = Annotated[AsyncClient, Depends(get_http_client)]
@@ -0,0 +1,11 @@
1
+ from fastapi import HTTPException, status
2
+
3
+
4
+ class LimitExceeded(HTTPException):
5
+ def __init__(self) -> None:
6
+ super().__init__(status.HTTP_403_FORBIDDEN, 'Limit exceeded')
7
+
8
+
9
+ class NotFound(HTTPException):
10
+ def __init__(self, name: str) -> None:
11
+ super().__init__(status.HTTP_404_NOT_FOUND, f'{name} not found')
@@ -0,0 +1,55 @@
1
+ import asyncio
2
+ from collections.abc import AsyncGenerator, Awaitable, Callable
3
+ from contextlib import asynccontextmanager
4
+ from typing import Any, cast
5
+
6
+ from fastapi import FastAPI
7
+ from starlette.datastructures import State
8
+
9
+ from . import settings
10
+
11
+ type CleanUpFunc = Callable[[], Awaitable[None]]
12
+
13
+
14
+ def init_cache(state: State, cache_url: str) -> CleanUpFunc:
15
+ from redis.asyncio.client import Redis
16
+
17
+ state.cache = Redis.from_url(cache_url)
18
+ return cast(CleanUpFunc, state.cache.aclose)
19
+
20
+
21
+ def init_db(state: State, db_url: str) -> CleanUpFunc:
22
+ from sqlalchemy.ext.asyncio import create_async_engine
23
+
24
+ state.db = create_async_engine(db_url)
25
+ return cast(CleanUpFunc, state.db.dispose)
26
+
27
+
28
+ def init_http_client(state: State, retries: int) -> CleanUpFunc:
29
+ from httpx import AsyncClient, AsyncHTTPTransport
30
+
31
+ state.http_client = AsyncClient(
32
+ transport=AsyncHTTPTransport(retries=retries)
33
+ )
34
+ return cast(CleanUpFunc, state.http_client.aclose)
35
+
36
+
37
+ INIT_FUNCS: dict[str, Callable[[State, Any], CleanUpFunc]] = {
38
+ 'cache_url': init_cache,
39
+ 'db_url': init_db,
40
+ 'http_retries': init_http_client,
41
+ }
42
+
43
+
44
+ @asynccontextmanager
45
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
46
+ clean_up_funcs: list[CleanUpFunc] = []
47
+
48
+ for attr, init_func in INIT_FUNCS.items():
49
+ value = getattr(settings, attr, None)
50
+ if value:
51
+ clean_up_funcs.append(init_func(app.state, value))
52
+
53
+ yield
54
+
55
+ await asyncio.gather(*(func() for func in clean_up_funcs))
@@ -0,0 +1,19 @@
1
+ from redis.asyncio.client import Redis
2
+ from redis.asyncio.lock import Lock as RedisLock
3
+
4
+ from . import settings
5
+
6
+
7
+ class Lock:
8
+ prefix: str = f'{settings.app_name}:lock' # type: ignore[has-type]
9
+ template: str = ''
10
+ timeout: float | None = None
11
+
12
+ # https://github.com/python/mypy/issues/15182
13
+ def __new__( # type: ignore[misc]
14
+ cls,
15
+ cache: Redis,
16
+ *args: str,
17
+ ) -> RedisLock:
18
+ name = f'{cls.prefix}:{cls.template.format(*args)}'
19
+ return cache.lock(name, timeout=cls.timeout)
@@ -0,0 +1,7 @@
1
+ from core import make_app, run
2
+
3
+ app = make_app()
4
+
5
+ # for debugging purposes
6
+ if __name__ == '__main__':
7
+ run(app)
@@ -0,0 +1,20 @@
1
+ from . import settings
2
+
3
+
4
+ def init_sentry() -> None:
5
+ if not settings.sentry_dsn:
6
+ return
7
+
8
+ import logging
9
+
10
+ import sentry_sdk
11
+ from sentry_sdk.integrations.logging import LoggingIntegration
12
+
13
+ logging.captureWarnings(True)
14
+
15
+ sentry_sdk.init(
16
+ dsn=settings.sentry_dsn,
17
+ environment=settings.app_env,
18
+ traces_sample_rate=settings.traces_sample_rate,
19
+ integrations=[LoggingIntegration(event_level=logging.WARNING)],
20
+ )