fastabc 0.2.2__tar.gz → 4.6.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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fastabc
3
- Version: 0.2.2
4
- Summary: Initialize and create app|server
3
+ Version: 4.6.0
4
+ Summary: Base classes for FastAPI applications with Sanic compatibility layer
5
5
  License: MIT
6
6
  Author: Aleksandr Yurlov
7
7
  Author-email: sasha.yur@mail.ru
@@ -12,5 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: fastapi (>=0.70.0,<0.71.0)
16
- Requires-Dist: uvicorn (>=0.15.0,<0.16.0)
15
+ Provides-Extra: proxy
16
+ Requires-Dist: fastapi (>=0.100.0)
17
+ Requires-Dist: httpx (>=0.24.0) ; extra == "proxy"
18
+ Requires-Dist: uvicorn (>=0.15.0)
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ from .baseclasses import Api, App
6
+
7
+ __version__ = "4.6.0"
8
+ __all__ = ["Api", "App"]
9
+
10
+
11
+ def start_server(app: App, create_fn: t.Callable[[], App], **config) -> None:
12
+ if config.get("single_process", True):
13
+ return app.run_single(**config)
14
+
15
+ app.loader(create_fn, **config)
16
+ return app.run_single(**config)
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import typing as t
6
+ from logging.config import dictConfig
7
+
8
+ from fastapi import APIRouter, FastAPI
9
+
10
+ from .structures import FastAPIConfig
11
+
12
+
13
+ class AppConfig:
14
+ """Sanic-совместимая обёртка для app.config.
15
+
16
+ Хранит конфигурацию в upper-case как в Sanic и предоставляет метод update_config().
17
+ """
18
+
19
+ def __init__(self):
20
+ self._data: dict = {}
21
+
22
+ def update_config(self, config) -> None:
23
+ if hasattr(config, "to_dict"):
24
+ data = config.to_dict(uppercase=True) if self._supports_uppercase(config) else config.to_dict()
25
+ data = {k.upper(): v for k, v in data.items()}
26
+ elif isinstance(config, dict):
27
+ data = {k.upper(): v for k, v in config.items()}
28
+ else:
29
+ return
30
+ self._data.update(data)
31
+
32
+ @staticmethod
33
+ def _supports_uppercase(config) -> bool:
34
+ try:
35
+ return "uppercase" in inspect.signature(config.to_dict).parameters
36
+ except (ValueError, TypeError):
37
+ return False
38
+
39
+ def get(self, key: str, default=None):
40
+ return self._data.get(key.upper(), default)
41
+
42
+ def __getitem__(self, key: str):
43
+ return self._data[key.upper()]
44
+
45
+ def __setitem__(self, key: str, value):
46
+ self._data[key.upper()] = value
47
+
48
+ def __contains__(self, key: str) -> bool:
49
+ return key.upper() in self._data
50
+
51
+ def __iter__(self):
52
+ return iter(self._data)
53
+
54
+ def __repr__(self):
55
+ return repr(self._data)
56
+
57
+ def items(self):
58
+ return self._data.items()
59
+
60
+ def keys(self):
61
+ return self._data.keys()
62
+
63
+ def values(self):
64
+ return self._data.values()
65
+
66
+ def to_dict(self) -> dict:
67
+ return dict(self._data)
68
+
69
+
70
+ class App(FastAPI):
71
+
72
+ def __init__(
73
+ self,
74
+ *args,
75
+ name: str | None = None,
76
+ log_config=None,
77
+ **kwargs,
78
+ ):
79
+ if name is not None and "title" not in kwargs:
80
+ kwargs["title"] = name
81
+ super().__init__(*args, **kwargs)
82
+ self._name = name or kwargs.get("title") or self.__class__.__name__
83
+ self.state.extensions = {}
84
+ self.state._config = AppConfig()
85
+
86
+ if log_config:
87
+ log_dict = log_config.to_dict() if hasattr(log_config, "to_dict") else log_config
88
+ if log_dict:
89
+ dictConfig(log_dict)
90
+
91
+ @property
92
+ def name(self) -> str:
93
+ return self._name
94
+
95
+ def loader(self, create_fn: t.Callable, **kwargs):
96
+ """Аналог Sanic AppLoader — сохраняет фабрику для multi-worker.
97
+ В single_process режиме не используется (start_server вызывает run_single напрямую).
98
+ """
99
+ self._create_fn = create_fn
100
+ self._create_kwargs = kwargs
101
+ return self
102
+
103
+ def run_single(self, **kwargs):
104
+ import uvicorn
105
+
106
+ config = self.make_startup_config(**kwargs)
107
+ uvicorn_config = {
108
+ "host": config.host,
109
+ "port": config.port,
110
+ "log_level": "debug" if config.debug else "info",
111
+ }
112
+
113
+ for key in ["workers", "reload", "access_log"]:
114
+ if key in config.extra:
115
+ uvicorn_config[key] = config.extra[key]
116
+
117
+ uvicorn.run(self, log_config=None, **uvicorn_config)
118
+
119
+ def make_startup_config(self, **kwargs) -> FastAPIConfig:
120
+ return FastAPIConfig.create(
121
+ {
122
+ "host": kwargs.pop("host", "0.0.0.0"),
123
+ "port": kwargs.pop("port", 8000),
124
+ "debug": kwargs.pop("debug", False),
125
+ **kwargs,
126
+ }
127
+ )
128
+
129
+ def exception(self, *exceptions: type[BaseException]):
130
+ """Регистрация handler-а исключений.
131
+
132
+ Принимает BaseException (как Sanic), но внутри маппит на Exception —
133
+ starlette не позволяет хэндлить не-Exception классы.
134
+ """
135
+ def decorator(handler: t.Callable):
136
+ for exc in exceptions:
137
+ target = exc if isinstance(exc, type) and issubclass(exc, Exception) else Exception
138
+ self.add_exception_handler(target, handler)
139
+ return handler
140
+ return decorator
141
+
142
+ @property
143
+ def extensions(self):
144
+ return self.state.extensions
145
+
146
+ @property
147
+ def config(self) -> AppConfig:
148
+ return self.state._config
149
+
150
+ def update_config(self, config) -> None:
151
+ """Устаревший метод — используйте app.config.update_config()."""
152
+ self.config.update_config(config)
153
+
154
+ def signal(self, event: str):
155
+ """Аналог Sanic app.signal() — для совместимости.
156
+
157
+ Маппит события:
158
+ - 'server.init.*', 'server.startup.*' → on_startup (FastAPI)
159
+ - 'server.shutdown.*' → on_shutdown (FastAPI)
160
+
161
+ Хендлер может иметь Sanic сигнатуру async def handler(app, loop) —
162
+ обёртка вызовет его с нашим app и текущим event loop.
163
+ """
164
+ def decorator(handler: t.Callable):
165
+ wrapped = self._wrap_signal_handler(handler)
166
+ if "shutdown" in event:
167
+ self.router.on_shutdown.append(wrapped)
168
+ elif "startup" in event or "init" in event:
169
+ self.router.on_startup.append(wrapped)
170
+ return handler
171
+ return decorator
172
+
173
+ def _wrap_signal_handler(self, handler: t.Callable) -> t.Callable:
174
+ try:
175
+ sig = inspect.signature(handler)
176
+ nargs = len([p for p in sig.parameters.values() if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)])
177
+ except (ValueError, TypeError):
178
+ nargs = 0
179
+
180
+ async def wrapper():
181
+ if nargs >= 2:
182
+ loop = asyncio.get_running_loop()
183
+ result = handler(self, loop)
184
+ elif nargs == 1:
185
+ result = handler(self)
186
+ else:
187
+ result = handler()
188
+ if inspect.isawaitable(result):
189
+ await result
190
+
191
+ return wrapper
192
+
193
+
194
+ class Api(APIRouter):
195
+ def __init__(self, name: str = None, url_prefix: str = ""):
196
+ super().__init__(prefix=url_prefix)
197
+ self.name = name
198
+
199
+ def new_routes(self, routes: dict[str, type], strict: bool = True):
200
+ cache: dict[str, int] = {}
201
+
202
+ for url, resource in routes.items():
203
+ name = resource.__name__.lower()
204
+ cache[name] = cache.get(name, 0) + 1
205
+ resource_instance = resource()
206
+
207
+ route_name = name if cache.get(name, 0) <= 1 else f"{name}_{cache[name]}"
208
+
209
+ for method_name in ["get", "post", "put", "delete", "patch", "head", "options"]:
210
+ if hasattr(resource_instance, method_name):
211
+ method = getattr(resource_instance, method_name)
212
+ self.add_api_route(
213
+ url, method, methods=[method_name.upper()], name=f"{route_name}_{method_name}"
214
+ )
215
+ return self
216
+
217
+ def init_app(self, app: FastAPI):
218
+ app.include_router(self)
219
+ return self
@@ -0,0 +1,21 @@
1
+ from enum import IntEnum
2
+
3
+ HTTP_ERROR_CODE_MIN = 400
4
+ HTTP_ERROR_CODE_MAX = 599
5
+
6
+
7
+ class HTTP_CODE(IntEnum): # noqa: N801
8
+ OK = 200
9
+ CREATED = 201
10
+ NO_CONTENT = 204
11
+ MOVED_PERMANENTLY = 301
12
+ BAD_REQUEST = 400
13
+ UNAUTHORIZED = 401
14
+ FORBIDDEN = 403
15
+ NOT_FOUND = 404
16
+ METHOD_NOT_ALLOWED = 405
17
+ CONFLICT = 409
18
+ INTERNAL_SERVER_ERROR = 500
19
+ BAD_GATEWAY = 502
20
+ SERVICE_UNAVAILABLE = 503
21
+ GATEWAY_TIMEOUT = 504
@@ -0,0 +1,3 @@
1
+ from jwt.exceptions import PyJWTError
2
+
3
+ __all__ = ["PyJWTError"]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from .auth import basic_auth, jwt_protection
4
+ from .headers import add_request_id, set_request_id
5
+ from .logging import log_request, log_response
6
+ from .proxy import redirect_trailing_slash, reverse_proxy
7
+
8
+
9
+ __all__ = [
10
+ "basic_auth",
11
+ "jwt_protection",
12
+ "add_request_id",
13
+ "set_request_id",
14
+ "log_request",
15
+ "log_response",
16
+ "redirect_trailing_slash",
17
+ "reverse_proxy",
18
+ ]
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from base64 import b64decode
4
+ from functools import partial
5
+ from typing import Callable
6
+
7
+ import jwt
8
+ from starlette.middleware.base import BaseHTTPMiddleware
9
+ from starlette.requests import Request
10
+ from starlette.responses import Response
11
+
12
+ from fastabc.middlewares.utils import check_secrets, unpack_basic_auth
13
+
14
+
15
+ def basic_auth(secrets: dict | None = None):
16
+ """
17
+ Middleware factory for Basic Auth.
18
+ Parses Authorization header and stores credential in request.state.basic
19
+ """
20
+ get_credential = partial(check_secrets, secrets=secrets)
21
+
22
+ class BasicAuthMiddleware(BaseHTTPMiddleware):
23
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
24
+ auth_header = request.headers.get("authorization", "")
25
+
26
+ if auth_header and auth_header.lower().startswith("basic "):
27
+ token = auth_header[6:]
28
+ if (data := unpack_basic_auth(token)) is not None:
29
+ try:
30
+ credential = get_credential(*str(b64decode(data), "utf-8").split(":"))
31
+ if credential is not None:
32
+ request.state.basic = credential
33
+ except (ValueError, UnicodeDecodeError):
34
+ pass
35
+
36
+ return await call_next(request)
37
+
38
+ return BasicAuthMiddleware
39
+
40
+
41
+ def jwt_protection(secret: str, algorithm: str = "HS256"):
42
+ """
43
+ Middleware factory for JWT protection.
44
+ Parses Bearer token from Authorization header and stores decoded JWT in request.state.jwt
45
+ """
46
+
47
+ class JWTMiddleware(BaseHTTPMiddleware):
48
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
49
+ auth_header = request.headers.get("authorization", "")
50
+
51
+ if auth_header and auth_header.lower().startswith("bearer "):
52
+ token = auth_header[7:]
53
+ try:
54
+ request.state.jwt = jwt.decode(token, secret, algorithms=[algorithm])
55
+ except jwt.PyJWTError:
56
+ pass
57
+
58
+ return await call_next(request)
59
+
60
+ return JWTMiddleware
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+ from uuid import uuid4
5
+
6
+ from starlette.middleware.base import BaseHTTPMiddleware
7
+ from starlette.requests import Request
8
+ from starlette.responses import Response
9
+
10
+
11
+ def add_request_id(generator: Callable[[], str] | None = None, override: bool = False):
12
+ """
13
+ Middleware factory for adding request ID to request.state.
14
+ If X-Request-ID header exists and override=False, uses header value.
15
+ Otherwise generates new ID using generator function.
16
+ """
17
+ id_generator = generator if generator is not None else lambda: str(uuid4())
18
+
19
+ class RequestIdMiddleware(BaseHTTPMiddleware):
20
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
21
+ request_id = request.headers.get("x-request-id")
22
+
23
+ if request_id and not override:
24
+ request.state.request_id = request_id
25
+ else:
26
+ request.state.request_id = id_generator()
27
+
28
+ return await call_next(request)
29
+
30
+ return RequestIdMiddleware
31
+
32
+
33
+ def set_request_id():
34
+ """
35
+ Middleware factory for setting X-Request-ID header in response.
36
+ Copies request_id from request.state to response headers.
37
+ """
38
+
39
+ class SetRequestIdMiddleware(BaseHTTPMiddleware):
40
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
41
+ response = await call_next(request)
42
+
43
+ if hasattr(request.state, "request_id"):
44
+ response.headers["x-request-id"] = request.state.request_id
45
+ elif "x-request-id" in request.headers:
46
+ response.headers["x-request-id"] = request.headers["x-request-id"]
47
+
48
+ return response
49
+
50
+ return SetRequestIdMiddleware
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+ from json import dumps
5
+ from logging import Logger
6
+ from time import monotonic
7
+
8
+ from starlette.middleware.base import BaseHTTPMiddleware
9
+ from starlette.requests import Request
10
+ from starlette.responses import Response
11
+
12
+ from fastabc.middlewares.utils import parse_user
13
+
14
+
15
+ def log_request(logger: Logger, get_user: t.Callable[[t.Optional[str]], t.Optional[str]] | None = None):
16
+ """
17
+ Middleware factory for logging incoming requests.
18
+ Logs IP, method, URI, user-agent, user, and request-id.
19
+ """
20
+ get_user_callback = get_user if get_user is not None else parse_user
21
+
22
+ class LogRequestMiddleware(BaseHTTPMiddleware):
23
+ async def dispatch(self, request: Request, call_next: t.Callable) -> Response:
24
+ request.state.start_time = monotonic()
25
+
26
+ # Extract token from Authorization header
27
+ auth_header = request.headers.get("authorization", "")
28
+ token = None
29
+ if auth_header.lower().startswith("bearer "):
30
+ token = auth_header[7:]
31
+
32
+ request.state.user = get_user_callback(token)
33
+
34
+ result = {
35
+ "ip": request.client.host if request.client else None,
36
+ "method": request.method,
37
+ "uri": request.url.path,
38
+ "user_agent": request.headers.get("user-agent"),
39
+ "user": getattr(request.state, "user", None),
40
+ "request-id": getattr(request.state, "request_id", None),
41
+ }
42
+
43
+ logger.info(dumps(result))
44
+
45
+ return await call_next(request)
46
+
47
+ return LogRequestMiddleware
48
+
49
+
50
+ def log_response(logger: Logger):
51
+ """
52
+ Middleware factory for logging responses.
53
+ Logs IP, method, URI, user, status, time, and request-id.
54
+ """
55
+
56
+ class LogResponseMiddleware(BaseHTTPMiddleware):
57
+ async def dispatch(self, request: Request, call_next: t.Callable) -> Response:
58
+ response = await call_next(request)
59
+
60
+ end_time = None
61
+ if hasattr(request.state, "start_time"):
62
+ end_time = round((monotonic() - request.state.start_time) * 1000)
63
+
64
+ result = {
65
+ "ip": request.client.host if request.client else None,
66
+ "method": request.method,
67
+ "uri": request.url.path,
68
+ "user": getattr(request.state, "user", None),
69
+ "status": response.status_code,
70
+ "time": f"{end_time}ms" if end_time is not None else "",
71
+ "request-id": getattr(request.state, "request_id", None),
72
+ }
73
+
74
+ if 400 <= response.status_code <= 599:
75
+ # Note: Can't easily get response body in BaseHTTPMiddleware
76
+ # Would need to use raw ASGI middleware for that
77
+ result["error"] = True
78
+
79
+ logger.info(dumps(result))
80
+
81
+ return response
82
+
83
+ return LogResponseMiddleware
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ import httpx
6
+ from starlette.middleware.base import BaseHTTPMiddleware
7
+ from starlette.requests import Request
8
+ from starlette.responses import RedirectResponse, Response
9
+
10
+
11
+ def redirect_trailing_slash(path: str):
12
+ """
13
+ Middleware factory for redirecting path without trailing slash to path with slash.
14
+
15
+ Args:
16
+ path: Exact path to match (e.g., "/webapp")
17
+
18
+ Example:
19
+ app.add_middleware(redirect_trailing_slash("/webapp"))
20
+
21
+ Request: GET /webapp?startapp=xxx
22
+ Redirect: GET /webapp/?startapp=xxx (307 redirect)
23
+ """
24
+
25
+ class RedirectTrailingSlashMiddleware(BaseHTTPMiddleware):
26
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
27
+ if request.url.path == path:
28
+ url = path + "/"
29
+ if request.url.query:
30
+ url = f"{url}?{request.url.query}"
31
+ return RedirectResponse(url=url, status_code=307)
32
+ return await call_next(request)
33
+
34
+ return RedirectTrailingSlashMiddleware
35
+
36
+
37
+ def reverse_proxy(path_prefix: str, target_url: str, strip_prefix: bool = False):
38
+ """
39
+ Middleware factory for reverse proxying requests to another service.
40
+
41
+ Args:
42
+ path_prefix: URL prefix to intercept (e.g., "/webapp/")
43
+ target_url: Target service URL (e.g., "http://localhost:3000")
44
+ strip_prefix: If True, removes path_prefix from proxied URL
45
+
46
+ Example:
47
+ app.add_middleware(reverse_proxy("/webapp/", "http://localhost:3000"))
48
+
49
+ Request: GET /webapp/index.html
50
+ Proxied: GET http://localhost:3000/webapp/index.html (strip_prefix=False)
51
+ Proxied: GET http://localhost:3000/index.html (strip_prefix=True)
52
+ """
53
+ target = target_url.rstrip("/")
54
+
55
+ class ReverseProxyMiddleware(BaseHTTPMiddleware):
56
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
57
+ path = request.url.path
58
+
59
+ if not path.startswith(path_prefix):
60
+ return await call_next(request)
61
+
62
+ # Build target URL
63
+ if strip_prefix:
64
+ proxy_path = path[len(path_prefix.rstrip("/")):]
65
+ else:
66
+ proxy_path = path
67
+
68
+ proxy_url = f"{target}{proxy_path}"
69
+ if request.url.query:
70
+ proxy_url = f"{proxy_url}?{request.url.query}"
71
+
72
+ # Forward headers (excluding hop-by-hop)
73
+ headers = {
74
+ k: v for k, v in request.headers.items()
75
+ if k.lower() not in ("host", "connection", "keep-alive", "transfer-encoding")
76
+ }
77
+
78
+ # Read body for non-GET requests
79
+ body = None
80
+ if request.method in ("POST", "PUT", "PATCH"):
81
+ body = await request.body()
82
+
83
+ try:
84
+ async with httpx.AsyncClient(timeout=30.0) as client:
85
+ response = await client.request(
86
+ method=request.method,
87
+ url=proxy_url,
88
+ headers=headers,
89
+ content=body,
90
+ )
91
+
92
+ # Filter response headers
93
+ response_headers = {
94
+ k: v for k, v in response.headers.items()
95
+ if k.lower() not in ("content-encoding", "content-length", "transfer-encoding")
96
+ }
97
+
98
+ return Response(
99
+ content=response.content,
100
+ status_code=response.status_code,
101
+ headers=response_headers,
102
+ media_type=response.headers.get("content-type"),
103
+ )
104
+
105
+ except httpx.ConnectError:
106
+ return Response(
107
+ content=b"Proxy target unavailable",
108
+ status_code=502,
109
+ media_type="text/plain",
110
+ )
111
+ except httpx.TimeoutException:
112
+ return Response(
113
+ content=b"Proxy target timeout",
114
+ status_code=504,
115
+ media_type="text/plain",
116
+ )
117
+
118
+ return ReverseProxyMiddleware
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import re
5
+ import typing as t
6
+ from types import MappingProxyType
7
+
8
+
9
+ def unpack_basic_auth(token: str) -> str | None:
10
+ if not token:
11
+ return None
12
+ if token.startswith("Basic "):
13
+ token = token[6:]
14
+ try:
15
+ return base64.b64decode(token).decode("utf-8")
16
+ except Exception:
17
+ return None
18
+
19
+
20
+ def check_secrets(login: str, password: str, secrets: dict | None = None) -> MappingProxyType | None:
21
+ if secrets is None:
22
+ return None
23
+ if login in secrets and secrets[login] == password:
24
+ return MappingProxyType({"username": login, "password": password})
25
+ return None
26
+
27
+
28
+ def parse_user(token: str | None) -> dict | None:
29
+ if token is None:
30
+ return None
31
+
32
+ try:
33
+ import jwt
34
+ payload = jwt.decode(token, options={"verify_signature": False})
35
+ uid = payload.get("sub") or payload.get("user_id") or payload.get("id")
36
+ return {"uid": uid, **payload} if uid else None
37
+ except Exception:
38
+ return None
@@ -1,4 +1,4 @@
1
- from dataclasses import dataclass, replace, field
1
+ from dataclasses import dataclass, field, replace
2
2
 
3
3
 
4
4
  @dataclass
@@ -29,4 +29,4 @@ class FastAPIConfig:
29
29
  "debug": self.debug,
30
30
  "access_log": self.access_log,
31
31
  **self.extra
32
- }
32
+ }
@@ -0,0 +1,25 @@
1
+ [tool.poetry]
2
+ name = "fastabc"
3
+ version = "4.6.0"
4
+ description = "Base classes for FastAPI applications with Sanic compatibility layer"
5
+ authors = ["Aleksandr Yurlov <sasha.yur@mail.ru>"]
6
+ license = "MIT"
7
+ packages = [
8
+ { include = "fastabc" }
9
+ ]
10
+
11
+ [tool.poetry.dependencies]
12
+ python = "^3.10"
13
+ fastapi = ">=0.100.0"
14
+ uvicorn = ">=0.15.0"
15
+ httpx = { version = ">=0.24.0", optional = true }
16
+
17
+ [tool.poetry.extras]
18
+ proxy = ["httpx"]
19
+
20
+ [tool.poetry.group.dev.dependencies]
21
+ build = "^1.2.2.post1"
22
+
23
+ [build-system]
24
+ requires = ["poetry-core>=1.0.0"]
25
+ build-backend = "poetry.core.masonry.api"
@@ -1,11 +0,0 @@
1
- from .baseclasses import App, Api
2
-
3
- __version__ = "4.2.0"
4
- __all__ = ["Api", "App"]
5
-
6
-
7
- def start_server(create_fn, **config):
8
- app_instance = App()
9
- app = app_instance.loader(create_fn, **config)
10
- import uvicorn
11
- uvicorn.run(app, host=config.get("host", "0.0.0.0"), port=config.get("port", 8000))
@@ -1,46 +0,0 @@
1
- import typing as t
2
- from fastapi import FastAPI, APIRouter
3
- from .structures import FastAPIConfig
4
-
5
- class App:
6
- def __init__(self, *args, lifespan: t.Optional[t.Callable] = None, **kwargs):
7
- if lifespan:
8
- kwargs["lifespan"] = lifespan
9
- self.app = FastAPI(*args, **kwargs)
10
- self.extensions = {}
11
-
12
- def loader(self, create_fn: t.Callable[[FastAPI], FastAPI], **kwargs):
13
- self.app = create_fn(self.app, **kwargs)
14
- return self.app
15
-
16
- def run_single(self, **kwargs):
17
- import uvicorn
18
- config = self.make_startup_config(**kwargs)
19
- uvicorn.run(self.app, host=config.host, port=config.port, debug=config.debug)
20
-
21
- def make_startup_config(self, **kwargs) -> FastAPIConfig:
22
- return FastAPIConfig.create({
23
- "host": kwargs.pop("host", "0.0.0.0"),
24
- "port": kwargs.pop("port", 8000),
25
- "debug": kwargs.pop("debug", False),
26
- **kwargs,
27
- })
28
-
29
-
30
- class Api:
31
- def __init__(self, name: str, url_prefix: str = ""):
32
- self.router = APIRouter(prefix=url_prefix)
33
- self.name = name
34
-
35
- def new_routes(self, routes: t.Dict[str, t.Type]):
36
- for url, resource in routes.items():
37
- resource_instance = resource()
38
- for method_name in ["get", "post", "put", "delete"]:
39
- if hasattr(resource_instance, method_name):
40
- method = getattr(resource_instance, method_name)
41
- self.router.add_api_route(url, method, methods=[method_name.upper()])
42
- return self
43
-
44
- def init_app(self, app: FastAPI):
45
- app.include_router(self.router)
46
- return self
@@ -1,21 +0,0 @@
1
- [tool.poetry]
2
- name = "fastabc"
3
- version = "0.2.2"
4
- description = "Initialize and create app|server"
5
- authors = ["Aleksandr Yurlov <sasha.yur@mail.ru>"]
6
- license = "MIT"
7
-
8
- [tool.poetry.dependencies]
9
- python = "^3.10"
10
- fastapi = "^0.70.0"
11
- uvicorn = "^0.15.0"
12
-
13
- [tool.poetry.scripts]
14
- fastapi_abc = "fastapi_abc.__init__:start_server"
15
-
16
- [tool.poetry.group.dev.dependencies]
17
- build = "^1.2.2.post1"
18
-
19
- [build-system]
20
- requires = ["poetry-core>=1.0.0"]
21
- build-backend = "poetry.core.masonry.api"