fastapi-ext-pkg 0.5.0__tar.gz → 0.7.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.
Files changed (34) hide show
  1. fastapi_ext_pkg-0.7.0/PKG-INFO +74 -0
  2. fastapi_ext_pkg-0.7.0/README.md +49 -0
  3. fastapi_ext_pkg-0.7.0/fastapi_ext_pkg.egg-info/PKG-INFO +74 -0
  4. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_ext_pkg.egg-info/SOURCES.txt +9 -0
  5. fastapi_ext_pkg-0.7.0/fastapi_ext_pkg.egg-info/requires.txt +5 -0
  6. fastapi_ext_pkg-0.7.0/fastapi_extras/apm/__init__.py +19 -0
  7. fastapi_ext_pkg-0.7.0/fastapi_extras/apm/config.py +66 -0
  8. fastapi_ext_pkg-0.7.0/fastapi_extras/apm/constants.py +46 -0
  9. fastapi_ext_pkg-0.7.0/fastapi_extras/apm/decorators.py +127 -0
  10. fastapi_ext_pkg-0.7.0/fastapi_extras/apm/labels.py +20 -0
  11. fastapi_ext_pkg-0.7.0/fastapi_extras/apm/masking.py +87 -0
  12. fastapi_ext_pkg-0.7.0/fastapi_extras/apm/middleware.py +105 -0
  13. fastapi_ext_pkg-0.7.0/fastapi_extras/apm/processors.py +80 -0
  14. fastapi_ext_pkg-0.7.0/fastapi_extras/middlewares/__init__.py +3 -0
  15. fastapi_ext_pkg-0.7.0/fastapi_extras/middlewares/configure.py +98 -0
  16. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_extras/security/auth.py +38 -16
  17. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_extras/security/auth_async.py +17 -14
  18. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/pyproject.toml +4 -1
  19. fastapi_ext_pkg-0.5.0/PKG-INFO +0 -42
  20. fastapi_ext_pkg-0.5.0/README.md +0 -19
  21. fastapi_ext_pkg-0.5.0/fastapi_ext_pkg.egg-info/PKG-INFO +0 -42
  22. fastapi_ext_pkg-0.5.0/fastapi_ext_pkg.egg-info/requires.txt +0 -2
  23. fastapi_ext_pkg-0.5.0/fastapi_extras/security/__init__.py +0 -0
  24. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_ext_pkg.egg-info/dependency_links.txt +0 -0
  25. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_ext_pkg.egg-info/top_level.txt +0 -0
  26. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_extras/__init__.py +0 -0
  27. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_extras/databases/__init__.py +0 -0
  28. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_extras/databases/redis.py +0 -0
  29. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_extras/middlewares/remove_cloud_trace_headers.py +0 -0
  30. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_extras/middlewares/remove_path.py +0 -0
  31. {fastapi_ext_pkg-0.5.0/fastapi_extras/middlewares → fastapi_ext_pkg-0.7.0/fastapi_extras/orm}/__init__.py +0 -0
  32. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/fastapi_extras/orm/sqlalchemy.py +0 -0
  33. {fastapi_ext_pkg-0.5.0/fastapi_extras/orm → fastapi_ext_pkg-0.7.0/fastapi_extras/security}/__init__.py +0 -0
  34. {fastapi_ext_pkg-0.5.0 → fastapi_ext_pkg-0.7.0}/setup.cfg +0 -0
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-ext-pkg
3
+ Version: 0.7.0
4
+ Summary: A well-tailored utility set for FastAPI.
5
+ Author-email: Dotz Developers <devs-dotz@dotz.com>
6
+ Project-URL: Repository, https://github.com/DotzInc/fastapi-ext-pkg
7
+ Keywords: fastapi,utilities
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Internet
16
+ Classifier: Topic :: Software Development
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: fastapi
22
+ Requires-Dist: httpx
23
+ Provides-Extra: apm
24
+ Requires-Dist: elastic-apm<7,>=6.20; extra == "apm"
25
+
26
+ # FastAPI Extension Package
27
+
28
+ ![Tests](https://github.com/DotzInc/fastapi-ext-pkg/actions/workflows/tests.yml/badge.svg?event=push)
29
+ ![PyPI - Version](https://img.shields.io/pypi/v/fastapi-ext-pkg)
30
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastapi-ext-pkg)
31
+
32
+ A well-tailored utility set for FastAPI.
33
+
34
+ ## Requirements
35
+
36
+ * Python 3.8+
37
+
38
+ ## Installation
39
+
40
+ To install FastAPI Extension Package, use pip:
41
+
42
+ ```sh
43
+ pip install fastapi-ext-pkg
44
+ ```
45
+
46
+ ## Shared middleware configuration
47
+
48
+ `configure_middlewares` registers middleware on an existing FastAPI application
49
+ before startup. Configure service values centrally while preserving routers and
50
+ lifespan:
51
+
52
+ ```python
53
+ from fastapi_extras.middlewares import configure_middlewares
54
+
55
+ configure_middlewares(
56
+ app,
57
+ root_path=settings.ROOT_PATH,
58
+ apm_config=settings.ELASTIC_APM,
59
+ cors={"allow_origins": settings.ALLOW_ORIGINS},
60
+ )
61
+ ```
62
+
63
+ The default registration order is path removal, trace-header removal, APM,
64
+ `extra_middlewares`, then CORS. Incoming requests traverse it in reverse. Use
65
+ `order` to preserve a service-specific order; include every configured name
66
+ exactly once. `MiddlewareSpec` defines custom middleware and named `overrides`
67
+ for replacement or disabling (`enabled=False`).
68
+
69
+ Use `root_path=None`, `remove_cloud_trace_headers=False`, `apm_enabled=False`,
70
+ or `cors=None` to omit middleware. Pass either `apm_config` or `apm_client`.
71
+ APM uses the upstream `elasticapm.contrib.starlette.ElasticAPM` by default and
72
+ requires the optional `[apm]` dependency. This branch contains no custom APM
73
+ implementation. To register another implementation supplied by the caller, pass
74
+ `apm_middleware_class`; `apm_options` supplies its constructor options.
@@ -0,0 +1,49 @@
1
+ # FastAPI Extension Package
2
+
3
+ ![Tests](https://github.com/DotzInc/fastapi-ext-pkg/actions/workflows/tests.yml/badge.svg?event=push)
4
+ ![PyPI - Version](https://img.shields.io/pypi/v/fastapi-ext-pkg)
5
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastapi-ext-pkg)
6
+
7
+ A well-tailored utility set for FastAPI.
8
+
9
+ ## Requirements
10
+
11
+ * Python 3.8+
12
+
13
+ ## Installation
14
+
15
+ To install FastAPI Extension Package, use pip:
16
+
17
+ ```sh
18
+ pip install fastapi-ext-pkg
19
+ ```
20
+
21
+ ## Shared middleware configuration
22
+
23
+ `configure_middlewares` registers middleware on an existing FastAPI application
24
+ before startup. Configure service values centrally while preserving routers and
25
+ lifespan:
26
+
27
+ ```python
28
+ from fastapi_extras.middlewares import configure_middlewares
29
+
30
+ configure_middlewares(
31
+ app,
32
+ root_path=settings.ROOT_PATH,
33
+ apm_config=settings.ELASTIC_APM,
34
+ cors={"allow_origins": settings.ALLOW_ORIGINS},
35
+ )
36
+ ```
37
+
38
+ The default registration order is path removal, trace-header removal, APM,
39
+ `extra_middlewares`, then CORS. Incoming requests traverse it in reverse. Use
40
+ `order` to preserve a service-specific order; include every configured name
41
+ exactly once. `MiddlewareSpec` defines custom middleware and named `overrides`
42
+ for replacement or disabling (`enabled=False`).
43
+
44
+ Use `root_path=None`, `remove_cloud_trace_headers=False`, `apm_enabled=False`,
45
+ or `cors=None` to omit middleware. Pass either `apm_config` or `apm_client`.
46
+ APM uses the upstream `elasticapm.contrib.starlette.ElasticAPM` by default and
47
+ requires the optional `[apm]` dependency. This branch contains no custom APM
48
+ implementation. To register another implementation supplied by the caller, pass
49
+ `apm_middleware_class`; `apm_options` supplies its constructor options.
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-ext-pkg
3
+ Version: 0.7.0
4
+ Summary: A well-tailored utility set for FastAPI.
5
+ Author-email: Dotz Developers <devs-dotz@dotz.com>
6
+ Project-URL: Repository, https://github.com/DotzInc/fastapi-ext-pkg
7
+ Keywords: fastapi,utilities
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.8
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Internet
16
+ Classifier: Topic :: Software Development
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: fastapi
22
+ Requires-Dist: httpx
23
+ Provides-Extra: apm
24
+ Requires-Dist: elastic-apm<7,>=6.20; extra == "apm"
25
+
26
+ # FastAPI Extension Package
27
+
28
+ ![Tests](https://github.com/DotzInc/fastapi-ext-pkg/actions/workflows/tests.yml/badge.svg?event=push)
29
+ ![PyPI - Version](https://img.shields.io/pypi/v/fastapi-ext-pkg)
30
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastapi-ext-pkg)
31
+
32
+ A well-tailored utility set for FastAPI.
33
+
34
+ ## Requirements
35
+
36
+ * Python 3.8+
37
+
38
+ ## Installation
39
+
40
+ To install FastAPI Extension Package, use pip:
41
+
42
+ ```sh
43
+ pip install fastapi-ext-pkg
44
+ ```
45
+
46
+ ## Shared middleware configuration
47
+
48
+ `configure_middlewares` registers middleware on an existing FastAPI application
49
+ before startup. Configure service values centrally while preserving routers and
50
+ lifespan:
51
+
52
+ ```python
53
+ from fastapi_extras.middlewares import configure_middlewares
54
+
55
+ configure_middlewares(
56
+ app,
57
+ root_path=settings.ROOT_PATH,
58
+ apm_config=settings.ELASTIC_APM,
59
+ cors={"allow_origins": settings.ALLOW_ORIGINS},
60
+ )
61
+ ```
62
+
63
+ The default registration order is path removal, trace-header removal, APM,
64
+ `extra_middlewares`, then CORS. Incoming requests traverse it in reverse. Use
65
+ `order` to preserve a service-specific order; include every configured name
66
+ exactly once. `MiddlewareSpec` defines custom middleware and named `overrides`
67
+ for replacement or disabling (`enabled=False`).
68
+
69
+ Use `root_path=None`, `remove_cloud_trace_headers=False`, `apm_enabled=False`,
70
+ or `cors=None` to omit middleware. Pass either `apm_config` or `apm_client`.
71
+ APM uses the upstream `elasticapm.contrib.starlette.ElasticAPM` by default and
72
+ requires the optional `[apm]` dependency. This branch contains no custom APM
73
+ implementation. To register another implementation supplied by the caller, pass
74
+ `apm_middleware_class`; `apm_options` supplies its constructor options.
@@ -6,9 +6,18 @@ fastapi_ext_pkg.egg-info/dependency_links.txt
6
6
  fastapi_ext_pkg.egg-info/requires.txt
7
7
  fastapi_ext_pkg.egg-info/top_level.txt
8
8
  fastapi_extras/__init__.py
9
+ fastapi_extras/apm/__init__.py
10
+ fastapi_extras/apm/config.py
11
+ fastapi_extras/apm/constants.py
12
+ fastapi_extras/apm/decorators.py
13
+ fastapi_extras/apm/labels.py
14
+ fastapi_extras/apm/masking.py
15
+ fastapi_extras/apm/middleware.py
16
+ fastapi_extras/apm/processors.py
9
17
  fastapi_extras/databases/__init__.py
10
18
  fastapi_extras/databases/redis.py
11
19
  fastapi_extras/middlewares/__init__.py
20
+ fastapi_extras/middlewares/configure.py
12
21
  fastapi_extras/middlewares/remove_cloud_trace_headers.py
13
22
  fastapi_extras/middlewares/remove_path.py
14
23
  fastapi_extras/orm/__init__.py
@@ -0,0 +1,5 @@
1
+ fastapi
2
+ httpx
3
+
4
+ [apm]
5
+ elastic-apm<7,>=6.20
@@ -0,0 +1,19 @@
1
+ from fastapi_extras.apm.config import build_apm_config
2
+ from fastapi_extras.apm.constants import DEFAULT_SANITIZE_FIELD_NAMES
3
+ from fastapi_extras.apm.decorators import capture_http_request
4
+ from fastapi_extras.apm.labels import label_transaction
5
+ from fastapi_extras.apm.masking import mask_sensitive_data, normalize_span_value
6
+ from fastapi_extras.apm.middleware import ElasticAPMMiddleware
7
+ from fastapi_extras.apm.processors import ALL_PROCESSORS, PROCESSORS
8
+
9
+ __all__ = [
10
+ "ALL_PROCESSORS",
11
+ "DEFAULT_SANITIZE_FIELD_NAMES",
12
+ "PROCESSORS",
13
+ "ElasticAPMMiddleware",
14
+ "build_apm_config",
15
+ "capture_http_request",
16
+ "label_transaction",
17
+ "mask_sensitive_data",
18
+ "normalize_span_value",
19
+ ]
@@ -0,0 +1,66 @@
1
+ from typing import Any, Dict, Iterable, List, Optional, Union
2
+
3
+ import fastapi
4
+
5
+ from fastapi_extras.apm.constants import (
6
+ DEFAULT_CAPTURE_BODY,
7
+ DEFAULT_CLOUD_PROVIDER,
8
+ DEFAULT_LOG_LEVEL,
9
+ DEFAULT_SANITIZE_FIELD_NAMES,
10
+ )
11
+ from fastapi_extras.apm.processors import ALL_PROCESSORS
12
+
13
+ FieldNamesInput = Union[str, Iterable[str]]
14
+
15
+
16
+ def parse_field_names(value: FieldNamesInput) -> List[str]:
17
+ items = value.split(",") if isinstance(value, str) else value
18
+ return [item.strip() for item in items if item.strip()]
19
+
20
+
21
+ def unique(items: Iterable[str]) -> List[str]:
22
+ return list(dict.fromkeys(items))
23
+
24
+
25
+ def has_credentials(server_url: Optional[str], secret_token: Optional[str]) -> bool:
26
+ return bool((server_url or "").strip() and (secret_token or "").strip())
27
+
28
+
29
+ def build_apm_config(
30
+ service_name: str,
31
+ *,
32
+ server_url: Optional[str] = "",
33
+ secret_token: Optional[str] = "",
34
+ environment: str = "",
35
+ enabled: Optional[bool] = None,
36
+ log_level: str = DEFAULT_LOG_LEVEL,
37
+ capture_body: str = DEFAULT_CAPTURE_BODY,
38
+ sanitize_field_names: Optional[FieldNamesInput] = None,
39
+ extra_sanitize_field_names: FieldNamesInput = (),
40
+ extra_processors: Iterable[str] = (),
41
+ **overrides: Any,
42
+ ) -> Dict[str, Any]:
43
+ base_field_names = (
44
+ DEFAULT_SANITIZE_FIELD_NAMES
45
+ if sanitize_field_names is None
46
+ else parse_field_names(sanitize_field_names)
47
+ )
48
+ config: Dict[str, Any] = {
49
+ "ENABLED": has_credentials(server_url, secret_token) if enabled is None else enabled,
50
+ "SERVICE_NAME": service_name,
51
+ "SECRET_TOKEN": secret_token or "",
52
+ "SERVER_URL": server_url or "",
53
+ "ENVIRONMENT": environment,
54
+ "LOG_LEVEL": log_level,
55
+ "CLOUD_PROVIDER": DEFAULT_CLOUD_PROVIDER,
56
+ "FRAMEWORK_NAME": "fastapi",
57
+ "FRAMEWORK_VERSION": fastapi.__version__,
58
+ "CAPTURE_HEADERS": True,
59
+ "CAPTURE_BODY": capture_body,
60
+ "SANITIZE_FIELD_NAMES": unique(
61
+ [*base_field_names, *parse_field_names(extra_sanitize_field_names)]
62
+ ),
63
+ "PROCESSORS": (*ALL_PROCESSORS, *extra_processors),
64
+ }
65
+ config.update({key.upper(): value for key, value in overrides.items()})
66
+ return config
@@ -0,0 +1,46 @@
1
+ EXTERNAL = "external"
2
+ UNKNOWN = "UNKNOWN"
3
+ UNKNOWN_STATUS_CODE = 999
4
+ MAX_FIELD_LENGTH = 500
5
+ MAX_RESPONSE_BODY_BYTES = 64 * 1024
6
+ TRUNCATED_SUFFIX = "… [TRUNCATED]"
7
+ UNREADABLE_RESPONSE = "<could not decode response>"
8
+ RESPONSE_BODY_TOO_LARGE = "<response body too large>"
9
+ RESPONSE_BODY_CONTEXT_KEY = "response_body"
10
+ JSON_MEDIA_TYPE = "application/json"
11
+ JSON_MEDIA_TYPE_SUFFIX = "+json"
12
+ IDENTITY_ENCODING = "identity"
13
+ DEFAULT_SANITIZE_FIELD_NAMES = (
14
+ "password",
15
+ "passwd",
16
+ "pwd",
17
+ "secret",
18
+ "*key",
19
+ "*token*",
20
+ "*session*",
21
+ "*credit*",
22
+ "*card*",
23
+ "*auth*",
24
+ "*principal*",
25
+ "set-cookie",
26
+ "pin",
27
+ "otp",
28
+ "*document*",
29
+ "*cpf*",
30
+ "*cnpj*",
31
+ "*email*",
32
+ "*phone*",
33
+ "*mobile*",
34
+ "*birth*",
35
+ "mother_name",
36
+ "rg",
37
+ "*account_number*",
38
+ "agency",
39
+ "pix_key",
40
+ "*cvv*",
41
+ "*salary*",
42
+ "*income*",
43
+ )
44
+ DEFAULT_LOG_LEVEL = "INFO"
45
+ DEFAULT_CAPTURE_BODY = "off"
46
+ DEFAULT_CLOUD_PROVIDER = "gcp"
@@ -0,0 +1,127 @@
1
+ import functools
2
+ import inspect
3
+ from typing import Any, Awaitable, Callable, Dict, Mapping, Optional, Tuple, TypeVar
4
+ from urllib.parse import urlparse
5
+
6
+ import elasticapm
7
+ from elasticapm.conf.constants import MASK
8
+
9
+ from fastapi_extras.apm.constants import EXTERNAL, UNKNOWN, UNKNOWN_STATUS_CODE, UNREADABLE_RESPONSE
10
+ from fastapi_extras.apm.labels import to_label_value
11
+ from fastapi_extras.apm.masking import (
12
+ FieldNames,
13
+ get_sanitize_field_names,
14
+ is_sensitive_key,
15
+ mask_sensitive_data,
16
+ normalize_span_value,
17
+ )
18
+
19
+ ReturnType = TypeVar("ReturnType")
20
+
21
+ ERROR_BODY_ATTRIBUTES = ("errors", "content", "detail")
22
+
23
+
24
+ def get_span_name(method: str, url: str) -> str:
25
+ parsed = urlparse(url)
26
+ path = parsed.path
27
+ if parsed.query:
28
+ path = f"{path}?{parsed.query}"
29
+ return f"{method.upper()} {path}"
30
+
31
+
32
+ def is_outgoing_request(value: Any) -> bool:
33
+ return isinstance(getattr(value, "method", None), str) and isinstance(
34
+ getattr(value, "url", None), str
35
+ )
36
+
37
+
38
+ def find_outgoing_request(args: Tuple[Any, ...], kwargs: Mapping[str, Any]) -> Any:
39
+ candidate = kwargs.get("request")
40
+ if is_outgoing_request(candidate):
41
+ return candidate
42
+ return next((arg for arg in args if is_outgoing_request(arg)), None)
43
+
44
+
45
+ async def read_response(response: Any) -> Tuple[Optional[int], Any]:
46
+ status_code = getattr(response, "status", None)
47
+ if not isinstance(status_code, int):
48
+ status_code = getattr(response, "status_code", None)
49
+ if not isinstance(status_code, int):
50
+ return None, None
51
+ try:
52
+ text = response.text
53
+ body = text() if callable(text) else text
54
+ if inspect.isawaitable(body):
55
+ body = await body
56
+ except Exception:
57
+ body = UNREADABLE_RESPONSE
58
+ return status_code, body
59
+
60
+
61
+ def read_error(error: Exception) -> Tuple[int, Any]:
62
+ status_code = getattr(error, "status_code", None)
63
+ if not isinstance(status_code, int):
64
+ return UNKNOWN_STATUS_CODE, repr(error)
65
+ body = {}
66
+ for attribute in ERROR_BODY_ATTRIBUTES:
67
+ value = getattr(error, attribute, None)
68
+ if value:
69
+ body[attribute] = value
70
+ return status_code, body
71
+
72
+
73
+ def build_span_labels(
74
+ status_code: Optional[int],
75
+ response_body: Any,
76
+ payload: Any,
77
+ extra_labels: Optional[Mapping[str, Any]] = None,
78
+ field_names: Optional[FieldNames] = None,
79
+ ) -> Dict[str, Any]:
80
+ names = get_sanitize_field_names() if field_names is None else field_names
81
+ labels: Dict[str, Any] = {
82
+ "status_code": status_code,
83
+ "response_body": normalize_span_value(response_body),
84
+ }
85
+ if payload:
86
+ labels["payload"] = normalize_span_value(payload)
87
+ if extra_labels:
88
+ labels.update(extra_labels)
89
+ return {
90
+ key: MASK
91
+ if is_sensitive_key(key, names)
92
+ else to_label_value(mask_sensitive_data(value, names))
93
+ for key, value in labels.items()
94
+ }
95
+
96
+
97
+ def capture_http_request(
98
+ func: Callable[..., Awaitable[ReturnType]],
99
+ ) -> Callable[..., Awaitable[ReturnType]]:
100
+ @functools.wraps(func)
101
+ async def wrapper(*args: Any, **kwargs: Any) -> ReturnType:
102
+ request = find_outgoing_request(args, kwargs)
103
+ method = getattr(request, "method", UNKNOWN)
104
+ url = getattr(request, "url", UNKNOWN)
105
+ payload = getattr(request, "payload", None)
106
+ status_code: Optional[int] = None
107
+ response_body: Any = None
108
+
109
+ async with elasticapm.async_capture_span(
110
+ get_span_name(method, url), span_type=EXTERNAL
111
+ ) as span:
112
+ try:
113
+ response = await func(*args, **kwargs)
114
+ status_code, response_body = await read_response(response)
115
+ return response
116
+ except Exception as error:
117
+ status_code, response_body = read_error(error)
118
+ raise
119
+ finally:
120
+ if span:
121
+ span.label(
122
+ **build_span_labels(
123
+ status_code, response_body, payload, kwargs.get("labels")
124
+ )
125
+ )
126
+
127
+ return wrapper
@@ -0,0 +1,20 @@
1
+ import json
2
+ from typing import Any, Iterable, Mapping
3
+
4
+ import elasticapm
5
+
6
+ from fastapi_extras.apm.masking import get_sanitize_field_names, sanitize, truncate
7
+
8
+
9
+ def to_label_value(value: Any) -> Any:
10
+ if isinstance(value, (dict, list)):
11
+ return json.dumps(value, ensure_ascii=False, default=str)
12
+ return value
13
+
14
+
15
+ def label_transaction(data: Mapping[str, Any], keys: Iterable[str]) -> None:
16
+ labels = {key: data[key] for key in keys if data.get(key) is not None}
17
+ if not labels:
18
+ return
19
+ safe_labels = truncate(sanitize(labels, get_sanitize_field_names()))
20
+ elasticapm.label(**{key: to_label_value(value) for key, value in safe_labels.items()})
@@ -0,0 +1,87 @@
1
+ import base64
2
+ import json
3
+ import logging
4
+ from typing import Any, Optional, Pattern, Sequence
5
+
6
+ import elasticapm
7
+ from elasticapm.conf.constants import BASE_SANITIZE_FIELD_NAMES, MASK
8
+ from pydantic import BaseModel
9
+
10
+ from fastapi_extras.apm.constants import MAX_FIELD_LENGTH, TRUNCATED_SUFFIX
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ FieldNames = Sequence[Pattern]
15
+
16
+
17
+ def get_sanitize_field_names() -> FieldNames:
18
+ client = elasticapm.get_client()
19
+ if client is None:
20
+ return BASE_SANITIZE_FIELD_NAMES
21
+ return client.config.sanitize_field_names
22
+
23
+
24
+ def is_sensitive_key(key: Any, field_names: FieldNames) -> bool:
25
+ if not isinstance(key, str):
26
+ return False
27
+ normalized = key.strip().lower()
28
+ return any(pattern.match(normalized) for pattern in field_names)
29
+
30
+
31
+ def sanitize(data: Any, field_names: FieldNames) -> Any:
32
+ if isinstance(data, dict):
33
+ return {
34
+ key: MASK if is_sensitive_key(key, field_names) else sanitize(value, field_names)
35
+ for key, value in data.items()
36
+ }
37
+ if isinstance(data, (list, tuple)):
38
+ return [sanitize(item, field_names) for item in data]
39
+ return data
40
+
41
+
42
+ def truncate(data: Any, max_length: int = MAX_FIELD_LENGTH) -> Any:
43
+ if isinstance(data, dict):
44
+ return {key: truncate(value, max_length) for key, value in data.items()}
45
+ if isinstance(data, (list, tuple)):
46
+ return [truncate(item, max_length) for item in data]
47
+ if data is None or isinstance(data, (bool, int, float)):
48
+ return data
49
+ text = data if isinstance(data, str) else str(data)
50
+ if len(text) > max_length:
51
+ return text[:max_length] + TRUNCATED_SUFFIX
52
+ return text
53
+
54
+
55
+ def normalize_span_value(value: Any) -> Any:
56
+ if isinstance(value, BaseModel):
57
+ dump = getattr(value, "model_dump", None) or value.dict
58
+ return dump()
59
+ if isinstance(value, (bytes, bytearray)):
60
+ try:
61
+ return bytes(value).decode("utf-8")
62
+ except UnicodeDecodeError:
63
+ return base64.b64encode(bytes(value)).decode("ascii")
64
+ return value
65
+
66
+
67
+ def parse_json(value: str) -> Any:
68
+ try:
69
+ return json.loads(value)
70
+ except ValueError:
71
+ return value
72
+
73
+
74
+ def mask_sensitive_data(
75
+ data: Any,
76
+ field_names: Optional[FieldNames] = None,
77
+ max_length: int = MAX_FIELD_LENGTH,
78
+ ) -> Any:
79
+ names = get_sanitize_field_names() if field_names is None else field_names
80
+ try:
81
+ value = normalize_span_value(data)
82
+ if isinstance(value, str):
83
+ value = parse_json(value)
84
+ return truncate(sanitize(value, names), max_length)
85
+ except Exception:
86
+ logger.exception("Could not mask APM data")
87
+ return MASK
@@ -0,0 +1,105 @@
1
+ import functools
2
+ from typing import Any, List, Optional
3
+
4
+ import elasticapm
5
+ from elasticapm.base import Client
6
+ from elasticapm.contrib.starlette import ElasticAPM
7
+ from starlette.datastructures import Headers
8
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
9
+
10
+ from fastapi_extras.apm.constants import (
11
+ IDENTITY_ENCODING,
12
+ JSON_MEDIA_TYPE,
13
+ JSON_MEDIA_TYPE_SUFFIX,
14
+ MAX_RESPONSE_BODY_BYTES,
15
+ RESPONSE_BODY_CONTEXT_KEY,
16
+ RESPONSE_BODY_TOO_LARGE,
17
+ )
18
+ from fastapi_extras.apm.masking import mask_sensitive_data
19
+
20
+
21
+ def is_json(headers: Headers) -> bool:
22
+ media_type = headers.get("content-type", "").split(";")[0].strip().lower()
23
+ return media_type == JSON_MEDIA_TYPE or media_type.endswith(JSON_MEDIA_TYPE_SUFFIX)
24
+
25
+
26
+ def is_encoded(headers: Headers) -> bool:
27
+ encoding = headers.get("content-encoding", IDENTITY_ENCODING).strip().lower()
28
+ return encoding not in ("", IDENTITY_ENCODING)
29
+
30
+
31
+ class ResponseBodyRecorder:
32
+ def __init__(self, max_bytes: int) -> None:
33
+ self.max_bytes = max_bytes
34
+ self.chunks: List[bytes] = []
35
+ self.size = 0
36
+ self.readable = False
37
+ self.too_large = False
38
+
39
+ def start(self, message: Message) -> None:
40
+ headers = Headers(raw=message.get("headers", []))
41
+ content_length = headers.get("content-length", "")
42
+ self.readable = is_json(headers) and not is_encoded(headers)
43
+ self.too_large = content_length.isdigit() and int(content_length) > self.max_bytes
44
+
45
+ def feed(self, message: Message) -> bool:
46
+ if self.readable and not self.too_large:
47
+ self._append(message.get("body", b""))
48
+ return not message.get("more_body", False)
49
+
50
+ def _append(self, chunk: bytes) -> None:
51
+ self.size += len(chunk)
52
+ if self.size > self.max_bytes:
53
+ self.too_large = True
54
+ self.chunks.clear()
55
+ elif chunk:
56
+ self.chunks.append(chunk)
57
+
58
+
59
+ class ElasticAPMMiddleware(ElasticAPM):
60
+ def __init__(
61
+ self,
62
+ app: ASGIApp,
63
+ client: Optional[Client] = None,
64
+ capture_response_body: bool = True,
65
+ max_response_body_bytes: int = MAX_RESPONSE_BODY_BYTES,
66
+ **kwargs: Any,
67
+ ) -> None:
68
+ super().__init__(app, client, **kwargs)
69
+ self.capture_response_body = capture_response_body
70
+ self.max_response_body_bytes = max_response_body_bytes
71
+
72
+ def _record_response_body(self, recorder: ResponseBodyRecorder) -> None:
73
+ if not recorder.readable:
74
+ return
75
+ if recorder.too_large:
76
+ body = RESPONSE_BODY_TOO_LARGE
77
+ else:
78
+ body = mask_sensitive_data(
79
+ b"".join(recorder.chunks), self.client.config.sanitize_field_names
80
+ )
81
+ elasticapm.set_custom_context({RESPONSE_BODY_CONTEXT_KEY: body})
82
+
83
+ def _should_record(self, scope: Scope) -> bool:
84
+ return (
85
+ scope["type"] == "http"
86
+ and self.capture_response_body
87
+ and not self.client.should_ignore_url(scope["path"])
88
+ )
89
+
90
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
91
+ if not self._should_record(scope):
92
+ await super().__call__(scope, receive, send)
93
+ return
94
+
95
+ recorder = ResponseBodyRecorder(self.max_response_body_bytes)
96
+
97
+ @functools.wraps(send)
98
+ async def recording_send(message: Message) -> None:
99
+ if message.get("type") == "http.response.start":
100
+ recorder.start(message)
101
+ elif message.get("type") == "http.response.body" and recorder.feed(message):
102
+ self._record_response_body(recorder)
103
+ await send(message)
104
+
105
+ await super().__call__(scope, receive, recording_send)
@@ -0,0 +1,80 @@
1
+ import json
2
+ from typing import Any, Dict, Optional
3
+
4
+ from elasticapm.conf.constants import ERROR, MASK, SPAN, TRANSACTION
5
+ from elasticapm.processors import for_events
6
+
7
+ from fastapi_extras.apm.masking import FieldNames, is_sensitive_key, sanitize
8
+
9
+ PROCESSORS = (
10
+ "fastapi_extras.apm.processors.sanitize_http_request_json_body",
11
+ "fastapi_extras.apm.processors.sanitize_custom_context",
12
+ "fastapi_extras.apm.processors.sanitize_labels",
13
+ )
14
+ AGENT_PROCESSORS = (
15
+ "elasticapm.processors.sanitize_stacktrace_locals",
16
+ "elasticapm.processors.sanitize_http_request_cookies",
17
+ "elasticapm.processors.sanitize_http_response_cookies",
18
+ "elasticapm.processors.sanitize_http_headers",
19
+ "elasticapm.processors.sanitize_http_wsgi_env",
20
+ "elasticapm.processors.sanitize_http_request_body",
21
+ )
22
+ ALL_PROCESSORS = (*PROCESSORS, *AGENT_PROCESSORS)
23
+
24
+
25
+ def load_json_structure(value: Any) -> Optional[Any]:
26
+ if isinstance(value, (dict, list)):
27
+ return value
28
+ if not isinstance(value, (str, bytes, bytearray)):
29
+ return None
30
+ try:
31
+ parsed = json.loads(value)
32
+ except ValueError:
33
+ return None
34
+ return parsed if isinstance(parsed, (dict, list)) else None
35
+
36
+
37
+ def sanitize_label(key: str, value: Any, field_names: FieldNames) -> Any:
38
+ if is_sensitive_key(key, field_names):
39
+ return MASK
40
+ if not isinstance(value, str):
41
+ return value
42
+ parsed = load_json_structure(value)
43
+ if parsed is None:
44
+ return value
45
+ return json.dumps(sanitize(parsed, field_names), ensure_ascii=False)
46
+
47
+
48
+ @for_events(ERROR, TRANSACTION)
49
+ def sanitize_http_request_json_body(client: Any, event: Dict[str, Any]) -> Dict[str, Any]:
50
+ try:
51
+ body = event["context"]["request"]["body"]
52
+ except (KeyError, TypeError):
53
+ return event
54
+ parsed = load_json_structure(body)
55
+ if parsed is None:
56
+ return event
57
+ event["context"]["request"]["body"] = json.dumps(
58
+ sanitize(parsed, client.config.sanitize_field_names)
59
+ )
60
+ return event
61
+
62
+
63
+ @for_events(ERROR, TRANSACTION)
64
+ def sanitize_custom_context(client: Any, event: Dict[str, Any]) -> Dict[str, Any]:
65
+ custom = (event.get("context") or {}).get("custom")
66
+ if isinstance(custom, dict):
67
+ event["context"]["custom"] = sanitize(custom, client.config.sanitize_field_names)
68
+ return event
69
+
70
+
71
+ @for_events(SPAN, TRANSACTION)
72
+ def sanitize_labels(client: Any, event: Dict[str, Any]) -> Dict[str, Any]:
73
+ tags = (event.get("context") or {}).get("tags")
74
+ if not isinstance(tags, dict):
75
+ return event
76
+ field_names = client.config.sanitize_field_names
77
+ event["context"]["tags"] = {
78
+ key: sanitize_label(key, value, field_names) for key, value in tags.items()
79
+ }
80
+ return event
@@ -0,0 +1,3 @@
1
+ from fastapi_extras.middlewares.configure import MiddlewareSpec, configure_middlewares
2
+
3
+ __all__ = ["MiddlewareSpec", "configure_middlewares"]
@@ -0,0 +1,98 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Mapping, Optional, Sequence, Type
3
+
4
+ from fastapi import FastAPI
5
+ from starlette.middleware.cors import CORSMiddleware
6
+
7
+ from fastapi_extras.middlewares.remove_cloud_trace_headers import RemoveCloudTraceHeadersMiddleware
8
+ from fastapi_extras.middlewares.remove_path import RemovePathMiddleware
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class MiddlewareSpec:
13
+ """Named middleware with constructor options and an explicit enabled flag."""
14
+
15
+ name: str
16
+ middleware_class: Type[Any]
17
+ options: Mapping[str, Any] = field(default_factory=dict)
18
+ enabled: bool = True
19
+
20
+
21
+ def configure_middlewares(
22
+ app: FastAPI,
23
+ *,
24
+ root_path: Optional[str] = None,
25
+ remove_cloud_trace_headers: bool = True,
26
+ apm_config: Optional[Mapping[str, Any]] = None,
27
+ apm_client: Any = None,
28
+ apm_enabled: bool = True,
29
+ apm_options: Optional[Mapping[str, Any]] = None,
30
+ apm_middleware_class: Optional[Type[Any]] = None,
31
+ cors: Optional[Mapping[str, Any]] = None,
32
+ extra_middlewares: Sequence[MiddlewareSpec] = (),
33
+ overrides: Sequence[MiddlewareSpec] = (),
34
+ order: Optional[Sequence[str]] = None,
35
+ ) -> None:
36
+ """Register middleware before startup, in add_middleware order.
37
+
38
+ Requests traverse this order in reverse. Extra middleware defaults to before
39
+ CORS. Overrides replace a named entry, including its enabled flag. None
40
+ disables optional path/APM/CORS entries. Existing middleware is preserved.
41
+ """
42
+ if app.middleware_stack is not None:
43
+ raise RuntimeError("Cannot configure middleware after application startup")
44
+ if getattr(app.state, "_fastapi_extras_middlewares_configured", False):
45
+ raise RuntimeError("Middleware has already been configured")
46
+ if apm_config is not None and apm_client is not None:
47
+ raise ValueError("Pass either apm_config or apm_client")
48
+
49
+ specs = {}
50
+ if root_path is not None:
51
+ specs["remove_path"] = MiddlewareSpec(
52
+ "remove_path", RemovePathMiddleware, {"path": root_path}
53
+ )
54
+ if remove_cloud_trace_headers:
55
+ specs["remove_cloud_trace_headers"] = MiddlewareSpec(
56
+ "remove_cloud_trace_headers", RemoveCloudTraceHeadersMiddleware
57
+ )
58
+ if apm_enabled and (apm_config is not None or apm_client is not None):
59
+ if apm_middleware_class is None:
60
+ from elasticapm.contrib.starlette import ElasticAPM
61
+
62
+ apm_middleware_class = ElasticAPM
63
+ specs["apm"] = MiddlewareSpec("apm", apm_middleware_class, dict(apm_options or {}))
64
+ for spec in extra_middlewares:
65
+ if spec.name in specs or spec.name in {
66
+ "remove_path",
67
+ "remove_cloud_trace_headers",
68
+ "apm",
69
+ "cors",
70
+ }:
71
+ raise ValueError("Duplicate middleware name: " + spec.name)
72
+ specs[spec.name] = spec
73
+ if cors is not None:
74
+ specs["cors"] = MiddlewareSpec("cors", CORSMiddleware, dict(cors))
75
+ for spec in overrides:
76
+ if spec.name not in specs:
77
+ raise ValueError("Unknown middleware override: " + spec.name)
78
+ specs[spec.name] = spec
79
+
80
+ names = list(specs) if order is None else list(order)
81
+ if len(names) != len(set(names)) or set(names) != set(specs):
82
+ raise ValueError("order must contain every configured middleware name exactly once")
83
+
84
+ # Validate the full plan before creating a client or modifying the app.
85
+ if "apm" in specs and specs["apm"].enabled:
86
+ if apm_client is None:
87
+ from elasticapm.contrib.starlette import make_apm_client
88
+
89
+ apm_client = make_apm_client(dict(apm_config))
90
+ spec = specs["apm"]
91
+ specs["apm"] = MiddlewareSpec(
92
+ spec.name, spec.middleware_class, {**spec.options, "client": apm_client}
93
+ )
94
+ for name in names:
95
+ spec = specs[name]
96
+ if spec.enabled:
97
+ app.add_middleware(spec.middleware_class, **dict(spec.options))
98
+ app.state._fastapi_extras_middlewares_configured = True
@@ -2,7 +2,7 @@ import hashlib
2
2
  import json
3
3
  import logging
4
4
  import typing
5
- from typing import Any, AsyncGenerator, Awaitable, Callable, Optional, Protocol, Union
5
+ from typing import Any, AsyncGenerator, Awaitable, Callable, Optional, Protocol, Tuple, Union
6
6
 
7
7
  import httpx
8
8
  from fastapi import Depends, HTTPException, Request, status
@@ -14,6 +14,12 @@ logger = logging.getLogger(__name__)
14
14
  ssl_context = httpx.create_ssl_context()
15
15
 
16
16
  UNAUTHORIZED = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
17
+ SERVICE_UNAVAILABLE = HTTPException(
18
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Authorization service unavailable"
19
+ )
20
+ DENIED_STATUS_CODES = frozenset(
21
+ {status.HTTP_400_BAD_REQUEST, status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN}
22
+ )
17
23
  DEFAULT_SCHEME = APIKeyHeader(name="Authorization")
18
24
  DEFAULT_CACHE_TTL = 60 # seconds
19
25
 
@@ -38,6 +44,21 @@ def keygen(seed: str, prefix: str = "") -> str:
38
44
  return f"{prefix}{hash}"
39
45
 
40
46
 
47
+ def parse_response(response: httpx.Response) -> Tuple[bool, Any]:
48
+ if response.status_code in DENIED_STATUS_CODES:
49
+ return False, None
50
+
51
+ if response.status_code != status.HTTP_200_OK:
52
+ logger.error(f"Authorization service returned unexpected status {response.status_code}")
53
+ raise SERVICE_UNAVAILABLE
54
+
55
+ try:
56
+ return True, response.json()
57
+ except ValueError:
58
+ logger.error(f"Authorization service returned a non-JSON body: {response.text}")
59
+ raise SERVICE_UNAVAILABLE
60
+
61
+
41
62
  def remote_authorization(
42
63
  url: Union[str, AnyHttpUrl],
43
64
  *,
@@ -60,7 +81,7 @@ def remote_authorization(
60
81
  try:
61
82
  cached = await cache.get(key)
62
83
  except Exception as cache_error:
63
- logger.warning("Cache get failed, proceeding without cache: %s", cache_error)
84
+ logger.warning(f"Cache get failed, proceeding without cache: {cache_error}")
64
85
 
65
86
  if cached:
66
87
  cached = json.loads(cached)
@@ -84,22 +105,23 @@ def remote_authorization(
84
105
  try:
85
106
  response = await client.post(str(url), json=info, **kwargs)
86
107
  except httpx.HTTPError as http_error:
87
- logger.error(http_error)
88
- raise UNAUTHORIZED
89
- else:
90
- authorized = response.status_code == status.HTTP_200_OK
91
- context = response.json()
108
+ logger.error(
109
+ f"Authorization request failed: {type(http_error).__name__}: {http_error}"
110
+ )
111
+ raise SERVICE_UNAVAILABLE
112
+
113
+ authorized, context = parse_response(response)
92
114
 
93
- try:
94
- value = json.dumps({"authorized": authorized, "context": context})
95
- await cache.set(key, value, cache_ttl)
96
- except Exception as cache_error:
97
- logger.warning("Cache set failed, proceeding without cache: %s", cache_error)
115
+ try:
116
+ value = json.dumps({"authorized": authorized, "context": context})
117
+ await cache.set(key, value, cache_ttl)
118
+ except Exception as cache_error:
119
+ logger.warning(f"Cache set failed, proceeding without cache: {cache_error}")
98
120
 
99
- if not authorized:
100
- raise UNAUTHORIZED
121
+ if not authorized:
122
+ raise UNAUTHORIZED
101
123
 
102
- request.scope["authorizer"] = context
103
- return request.scope["authorizer"]
124
+ request.scope["authorizer"] = context
125
+ return request.scope["authorizer"]
104
126
 
105
127
  return authorizer
@@ -14,6 +14,8 @@ from redis.exceptions import ConnectionError as RedisConnectionError
14
14
  from redis.exceptions import RedisError
15
15
  from typing_extensions import Annotated
16
16
 
17
+ from fastapi_extras.security.auth import SERVICE_UNAVAILABLE, parse_response
18
+
17
19
  logger = logging.getLogger(__name__)
18
20
  ssl_context = httpx.create_ssl_context()
19
21
 
@@ -155,23 +157,24 @@ def remote_authorization_async(
155
157
  try:
156
158
  response = await http_client.post(str(url), json=info, **request_kwargs)
157
159
  except httpx.HTTPError as http_error:
158
- logger.error(http_error)
159
- raise UNAUTHORIZED
160
- else:
161
- authorized = response.status_code == status.HTTP_200_OK
162
- context = response.json()
160
+ logger.error(
161
+ "Authorization request failed: %s: %s", type(http_error).__name__, http_error
162
+ )
163
+ raise SERVICE_UNAVAILABLE
163
164
 
164
- try:
165
- value = json.dumps({"authorized": authorized, "context": context})
166
- await cache.set(key, value, ex=cache_ttl)
167
- except RedisError as cache_error:
168
- logger.warning("Cache set failed, proceeding without cache: %s", cache_error)
165
+ authorized, context = parse_response(response)
169
166
 
170
- if not authorized:
171
- raise UNAUTHORIZED
167
+ try:
168
+ value = json.dumps({"authorized": authorized, "context": context})
169
+ await cache.set(key, value, ex=cache_ttl)
170
+ except RedisError as cache_error:
171
+ logger.warning("Cache set failed, proceeding without cache: %s", cache_error)
172
172
 
173
- request.scope["authorizer"] = context
174
- return request.scope["authorizer"]
173
+ if not authorized:
174
+ raise UNAUTHORIZED
175
+
176
+ request.scope["authorizer"] = context
177
+ return request.scope["authorizer"]
175
178
 
176
179
  authorizer.aclose = aclose
177
180
  return authorizer
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-ext-pkg"
7
- version = "0.5.0"
7
+ version = "0.7.0"
8
8
  description = "A well-tailored utility set for FastAPI."
9
9
  readme = {file = "README.md", content-type = "text/markdown"}
10
10
  authors = [
@@ -27,6 +27,9 @@ classifiers = [
27
27
  requires-python = ">=3.8"
28
28
  dependencies = ["fastapi", "httpx"]
29
29
 
30
+ [project.optional-dependencies]
31
+ apm = ["elastic-apm>=6.20,<7"]
32
+
30
33
  [project.urls]
31
34
  Repository = "https://github.com/DotzInc/fastapi-ext-pkg"
32
35
 
@@ -1,42 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: fastapi-ext-pkg
3
- Version: 0.5.0
4
- Summary: A well-tailored utility set for FastAPI.
5
- Author-email: Dotz Developers <devs-dotz@dotz.com>
6
- Project-URL: Repository, https://github.com/DotzInc/fastapi-ext-pkg
7
- Keywords: fastapi,utilities
8
- Classifier: Operating System :: OS Independent
9
- Classifier: Programming Language :: Python :: 3 :: Only
10
- Classifier: Programming Language :: Python :: 3.8
11
- Classifier: Programming Language :: Python :: 3.9
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Topic :: Internet
16
- Classifier: Topic :: Software Development
17
- Classifier: Intended Audience :: Developers
18
- Classifier: License :: OSI Approved :: MIT License
19
- Requires-Python: >=3.8
20
- Description-Content-Type: text/markdown
21
- Requires-Dist: fastapi
22
- Requires-Dist: httpx
23
-
24
- # FastAPI Extension Package
25
-
26
- ![Tests](https://github.com/DotzInc/fastapi-ext-pkg/actions/workflows/tests.yml/badge.svg?event=push)
27
- ![PyPI - Version](https://img.shields.io/pypi/v/fastapi-ext-pkg)
28
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastapi-ext-pkg)
29
-
30
- A well-tailored utility set for FastAPI.
31
-
32
- ## Requirements
33
-
34
- * Python 3.8+
35
-
36
- ## Installation
37
-
38
- To install FastAPI Extension Package, use pip:
39
-
40
- ```sh
41
- pip install fastapi-ext-pkg
42
- ```
@@ -1,19 +0,0 @@
1
- # FastAPI Extension Package
2
-
3
- ![Tests](https://github.com/DotzInc/fastapi-ext-pkg/actions/workflows/tests.yml/badge.svg?event=push)
4
- ![PyPI - Version](https://img.shields.io/pypi/v/fastapi-ext-pkg)
5
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastapi-ext-pkg)
6
-
7
- A well-tailored utility set for FastAPI.
8
-
9
- ## Requirements
10
-
11
- * Python 3.8+
12
-
13
- ## Installation
14
-
15
- To install FastAPI Extension Package, use pip:
16
-
17
- ```sh
18
- pip install fastapi-ext-pkg
19
- ```
@@ -1,42 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: fastapi-ext-pkg
3
- Version: 0.5.0
4
- Summary: A well-tailored utility set for FastAPI.
5
- Author-email: Dotz Developers <devs-dotz@dotz.com>
6
- Project-URL: Repository, https://github.com/DotzInc/fastapi-ext-pkg
7
- Keywords: fastapi,utilities
8
- Classifier: Operating System :: OS Independent
9
- Classifier: Programming Language :: Python :: 3 :: Only
10
- Classifier: Programming Language :: Python :: 3.8
11
- Classifier: Programming Language :: Python :: 3.9
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Topic :: Internet
16
- Classifier: Topic :: Software Development
17
- Classifier: Intended Audience :: Developers
18
- Classifier: License :: OSI Approved :: MIT License
19
- Requires-Python: >=3.8
20
- Description-Content-Type: text/markdown
21
- Requires-Dist: fastapi
22
- Requires-Dist: httpx
23
-
24
- # FastAPI Extension Package
25
-
26
- ![Tests](https://github.com/DotzInc/fastapi-ext-pkg/actions/workflows/tests.yml/badge.svg?event=push)
27
- ![PyPI - Version](https://img.shields.io/pypi/v/fastapi-ext-pkg)
28
- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fastapi-ext-pkg)
29
-
30
- A well-tailored utility set for FastAPI.
31
-
32
- ## Requirements
33
-
34
- * Python 3.8+
35
-
36
- ## Installation
37
-
38
- To install FastAPI Extension Package, use pip:
39
-
40
- ```sh
41
- pip install fastapi-ext-pkg
42
- ```
@@ -1,2 +0,0 @@
1
- fastapi
2
- httpx
File without changes