c2casgiutils 0.0.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,466 @@
1
+ Metadata-Version: 2.3
2
+ Name: c2casgiutils
3
+ Version: 0.0.0
4
+ Summary: Common utilities for Camptocamp ASGI applications
5
+ License: BSD-2-Clause
6
+ Keywords: sqlalchemy,asgi,fastapi
7
+ Author: Camptocamp
8
+ Author-email: info@camptocamp.com
9
+ Requires-Python: >=3.10
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Plugins
12
+ Classifier: Framework :: Pyramid
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
24
+ Classifier: Typing :: Typed
25
+ Provides-Extra: alembic
26
+ Provides-Extra: all
27
+ Provides-Extra: fastapi
28
+ Provides-Extra: prometheus
29
+ Provides-Extra: sentry
30
+ Provides-Extra: sqlalchemy
31
+ Requires-Dist: aiofile
32
+ Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
33
+ Requires-Dist: aiohttp
34
+ Requires-Dist: alembic ; extra == "alembic"
35
+ Requires-Dist: alembic ; extra == "all"
36
+ Requires-Dist: fastapi[standard] ; extra == "all"
37
+ Requires-Dist: fastapi[standard] ; extra == "fastapi"
38
+ Requires-Dist: prometheus-client
39
+ Requires-Dist: prometheus-fastapi-instrumentator
40
+ Requires-Dist: prometheus-fastapi-instrumentator ; extra == "all"
41
+ Requires-Dist: prometheus-fastapi-instrumentator ; extra == "prometheus"
42
+ Requires-Dist: pydantic-settings
43
+ Requires-Dist: pyjwt
44
+ Requires-Dist: redis
45
+ Requires-Dist: sentry-sdk[fastapi]
46
+ Requires-Dist: sentry-sdk[fastapi] ; extra == "all"
47
+ Requires-Dist: sentry-sdk[fastapi] ; extra == "sentry"
48
+ Requires-Dist: sqlalchemy ; extra == "alembic"
49
+ Requires-Dist: sqlalchemy ; extra == "all"
50
+ Requires-Dist: sqlalchemy ; extra == "sqlalchemy"
51
+ Project-URL: Bug Tracker, https://github.com/camptocamp/c2casgiutils/issues
52
+ Project-URL: Repository, https://github.com/camptocamp/c2casgiutils
53
+ Description-Content-Type: text/markdown
54
+
55
+ # Camptocamp ASGI Utils
56
+
57
+ This package provides a set of utilities to help you build ASGI applications with Python.
58
+
59
+ ## Stack
60
+
61
+ Stack that we consider that the project uses:
62
+
63
+ - [FastAPI](https://github.com/fastapi/fastapi)
64
+ - [uvicorn](https://www.uvicorn.org/)
65
+ - [SQLAlchemy](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html)
66
+ - [Redis](https://redis.readthedocs.io/en/stable/examples/asyncio_examples.html)
67
+ - [Prometheus FastAPI Instrumentator](https://github.com/trallnag/prometheus-fastapi-instrumentator)
68
+ - [Sentry](https://docs.sentry.io/platforms/python/integrations/fastapi/)
69
+ - [Pydantic settings](https://docs.pydantic.dev/latest/usage/settings/)
70
+
71
+ ## Environment variables
72
+
73
+ See: https://github.com/camptocamp/c2casgiutils/blob/master/c2casgiutils/config.py
74
+
75
+ ## Installation
76
+
77
+ ```bash
78
+ pip install c2casgiutils[all]
79
+ ```
80
+
81
+ Add in your application:
82
+
83
+ ```python
84
+ import c2casgiutils
85
+ from c2casgiutils import broadcast
86
+ from c2casgiutils import config
87
+ from prometheus_client import start_http_server
88
+ from prometheus_fastapi_instrumentator import Instrumentator
89
+ from contextlib import asynccontextmanager
90
+
91
+ @asynccontextmanager
92
+ async def _lifespan(main_app: FastAPI) -> None:
93
+ """Handle application lifespan events."""
94
+
95
+ _LOGGER.info("Starting the application")
96
+ await c2casgiutils.startup(main_app)
97
+
98
+ yield
99
+
100
+ app = FastAPI(title="My fastapi_app application", lifespan=_lifespan)
101
+
102
+ app.mount('/c2c', c2casgiutils.app)
103
+
104
+ # For security headers (and compression)
105
+
106
+ # Add TrustedHostMiddleware (should be first)
107
+ app.add_middleware(
108
+ TrustedHostMiddleware,
109
+ allowed_hosts=["*"], # Configure with specific hosts in production
110
+ )
111
+
112
+ # Add HTTPSRedirectMiddleware
113
+ if os.environ.get("HTTP", "False").lower() not in ["true", "1"]:
114
+ app.add_middleware(HTTPSRedirectMiddleware)
115
+
116
+ # Add GZipMiddleware
117
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
118
+
119
+ # Set all CORS origins enabled
120
+ app.add_middleware(
121
+ CORSMiddleware,
122
+ allow_origins=["*"],
123
+ allow_credentials=True,
124
+ allow_methods=["*"],
125
+ allow_headers=["*"],
126
+ )
127
+
128
+ app.add_middleware(headers.ArmorHeaderMiddleware)
129
+
130
+ # Get Prometheus HTTP server port from environment variable 9000 by default
131
+ start_http_server(config.settings.prometheus.port)
132
+
133
+ instrumentator = Instrumentator(should_instrument_requests_inprogress=True)
134
+ instrumentator.instrument(app)
135
+ ```
136
+
137
+ ## Broadcasting
138
+
139
+ To use the broadcasting you should do something like this:
140
+
141
+ ```python
142
+
143
+ import c2casgiutils
144
+
145
+
146
+ class BroadcastResponse(BaseModel):
147
+ """Response from broadcast endpoint."""
148
+
149
+ result: list[dict[str, Any]] | None = None
150
+
151
+
152
+ echo_handler: Callable[[], Awaitable[list[BroadcastResponse] | None]] = None # type: ignore[assignment]
153
+
154
+ # Create a handler that will receive broadcasts
155
+ async def echo_handler_() -> dict[str, Any]:
156
+ """Echo handler for broadcast messages."""
157
+ return {"message": "Broadcast echo"}
158
+
159
+ # Subscribe the handler to a channel on module import
160
+ @asynccontextmanager
161
+ async def _lifespan(main_app: FastAPI) -> None:
162
+ """Handle application lifespan events."""
163
+
164
+ _LOGGER.info("Starting the application")
165
+ await c2casgiutils.startup(main_app)
166
+
167
+ # Register the echo handler
168
+ global echo_handler # pylint: disable=global-statement
169
+ echo_handler = await broadcast.decorate(echo_handler_, expect_answers=True)
170
+
171
+ yield
172
+ ```
173
+
174
+ Then you can use the `echo_handler` function you will have the response of all the registered applications.
175
+
176
+ ## Health checks
177
+
178
+ The `health_checks` module provides a flexible system for checking the health of various components of your application. Health checks are exposed through a REST API endpoint at `/c2c/health_checks` and are also integrated with Prometheus metrics.
179
+
180
+ ### Basic Usage
181
+
182
+ To initialize health checks in your application:
183
+
184
+ ```python
185
+ from c2casgiutils import health_checks
186
+
187
+ # Add Redis health check
188
+ health_checks.FACTORY.add(health_checks.Redis(tags=["liveness", "redis", "all"]))
189
+
190
+ # Add SQLAlchemy database connection check
191
+ from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
192
+ health_checks.FACTORY.add(health_checks.SQLAlchemy(Session=your_async_sessionmaker, tags=["database", "all"]))
193
+
194
+ # Add Alembic migration version check
195
+ health_checks.FACTORY.add(health_checks.Alembic(
196
+ Session=your_async_sessionmaker,
197
+ config_file="alembic.ini",
198
+ tags=["migrations", "database", "all"]
199
+ ))
200
+ ```
201
+
202
+ ### Available Health Checks
203
+
204
+ The package provides several built-in health check implementations:
205
+
206
+ 1. **Redis**: Checks connectivity to Redis by pinging both master and slave instances
207
+ 2. **SQLAlchemy**: Verifies database connectivity by executing a simple query
208
+ 3. **Alembic**: Ensures the database schema is up-to-date with the latest migrations
209
+
210
+ ### Custom Health Checks
211
+
212
+ You can create custom health checks by extending the `Check` base class:
213
+
214
+ ```python
215
+ from c2casgiutils.health_checks import Check, Result
216
+
217
+ class MyCustomCheck(Check):
218
+ async def check(self) -> Result:
219
+ # Your check logic here
220
+ try:
221
+ # Perform your check...
222
+ return Result(status_code=200, payload={"message": "Everything is fine!"})
223
+ except Exception as e:
224
+ return Result(status_code=500, payload={"error": str(e)})
225
+
226
+ # Add your custom check
227
+ health_checks.FACTORY.add(MyCustomCheck(tags=["custom", "all"]))
228
+ ```
229
+
230
+ ### Filtering Health Checks
231
+
232
+ Health checks can be filtered using tags or names:
233
+
234
+ - **Tags**: Add relevant tags when creating a check to categorize it
235
+ - **API Filtering**: Use query parameters to filter checks when calling the API:
236
+ - `/c2c/health_checks?tags=database,critical` - Run only checks with "database" or "critical" tags
237
+ - `/c2c/health_checks?name=Redis` - Run only the Redis check
238
+
239
+ ### Prometheus Integration
240
+
241
+ Health check results are automatically exported to Prometheus metrics via the `health_checks_failure` gauge, allowing you to monitor and alert on health check failures.
242
+
243
+ ## Middleware
244
+
245
+ ### Headers Middleware
246
+
247
+ The `ArmorHeaderMiddleware` provides automatic security headers configuration for your ASGI application. It allows you to configure headers based on request netloc (host:port) and path patterns.
248
+
249
+ #### Basic Usage
250
+
251
+ ```python
252
+ from c2casgiutils.headers import ArmorHeaderMiddleware
253
+
254
+ # Use default security headers
255
+ app.add_middleware(ArmorHeaderMiddleware)
256
+
257
+ # Or with custom configuration
258
+ app.add_middleware(ArmorHeaderMiddleware, headers_config=your_custom_config)
259
+ ```
260
+
261
+ #### Default Security Headers
262
+
263
+ The middleware comes with sensible security defaults including:
264
+
265
+ - **Content-Security-Policy**: Restricts resource loading to prevent XSS attacks
266
+ - **X-Frame-Options**: Prevents clickjacking by denying iframe embedding
267
+ - **Strict-Transport-Security**: Forces HTTPS connections (disabled for localhost)
268
+ - **X-Content-Type-Options**: Prevents MIME-type sniffing
269
+ - **Referrer-Policy**: Controls referrer information sent with requests
270
+ - **Permissions-Policy**: Restricts access to browser features like geolocation
271
+ - **X-DNS-Prefetch-Control**: Disables DNS prefetching
272
+ - **Expect-CT**: Certificate Transparency enforcement
273
+ - **Origin-Agent-Cluster**: Isolates origin agent clusters
274
+ - **Cross-Origin policies**: CORP, COOP, COEP for cross-origin protection
275
+
276
+ #### Custom Configuration
277
+
278
+ You can configure headers based on request patterns:
279
+
280
+ ```python
281
+ from c2casgiutils.headers import ArmorHeaderMiddleware
282
+
283
+ custom_config = {
284
+ "api_endpoints": {
285
+ "path_match": r"^/api/.*", # Regex pattern for paths
286
+ "headers": {
287
+ "Access-Control-Allow-Origin": "*",
288
+ "X-Custom-Header": "api-value"
289
+ },
290
+ "order": 1 # Processing order
291
+ },
292
+ "admin_section": {
293
+ "netloc_match": r"^admin\..*", # Regex for host matching
294
+ "path_match": r"^/admin/.*",
295
+ "headers": {
296
+ "X-Robots-Tag": "noindex, nofollow"
297
+ },
298
+ "status_code": 200, # Only apply for specific status code
299
+ "order": 2
300
+ },
301
+ "success_responses": {
302
+ "headers": {
303
+ "Cache-Control": ["public", "max-age=3600"]
304
+ },
305
+ "status_code": (200, 299), # Apply for a range of status codes (200-299)
306
+ "order": 3
307
+ },
308
+ "api_methods": {
309
+ "path_match": r"^/api/.*",
310
+ "methods": ["GET", "HEAD"], # Only apply for specific HTTP methods
311
+ "headers": {
312
+ "Cache-Control": ["public", "max-age=3600"]
313
+ },
314
+ "order": 4
315
+ },
316
+ "remove_header": {
317
+ "headers": {
318
+ "Server": None # Remove header by setting to None
319
+ }
320
+ }
321
+ }
322
+
323
+ app.add_middleware(ArmorHeaderMiddleware, headers_config=custom_config)
324
+ ```
325
+
326
+ #### Header Value Types
327
+
328
+ Headers support multiple value types:
329
+
330
+ ```python
331
+ headers = {
332
+ # String value
333
+ "X-Custom": "value",
334
+
335
+ # List (joined with "; ")
336
+ "Cache-Control": ["no-cache", "no-store", "must-revalidate"],
337
+
338
+ # Dictionary (for complex headers like CSP)
339
+ "Content-Security-Policy": {
340
+ "default-src": ["'self'"],
341
+ "script-src": ["'self'", "https://cdn.example.com"],
342
+ "style-src": ["'self'", "'unsafe-inline'"]
343
+ },
344
+
345
+ # List (joined with ", ") for Permissions-Policy
346
+ "Permissions-Policy": ["geolocation=()", "microphone=()"],
347
+
348
+ # Remove header
349
+ "Unwanted-Header": None
350
+ }
351
+ ```
352
+
353
+ #### Special Localhost Handling
354
+
355
+ The middleware automatically disables `Strict-Transport-Security` for localhost to facilitate development.
356
+
357
+ #### Status Code Configuration
358
+
359
+ You can apply headers conditionally based on response status codes:
360
+
361
+ - Apply to a single status code: `"status_code": 200`
362
+ - Apply to a range of status codes: `"status_code": (200, 299)` (for all 2xx success responses)
363
+
364
+ This feature is useful for adding caching headers only to successful responses, or special headers for specific error codes.
365
+
366
+ #### HTTP Method Filtering
367
+
368
+ You can configure headers to be applied only for specific HTTP methods:
369
+
370
+ ```python
371
+ {
372
+ "api_post_endpoints": {
373
+ "path_match": r"^/api/.*",
374
+ "methods": ["POST", "PUT", "PATCH"], # Only apply for these methods
375
+ "headers": {
376
+ "Cache-Control": "no-store"
377
+ }
378
+ },
379
+ "api_get_endpoints": {
380
+ "path_match": r"^/api/.*",
381
+ "methods": ["GET", "HEAD"], # Only apply for GET and HEAD requests
382
+ "headers": {
383
+ "Cache-Control": ["public", "max-age=3600"]
384
+ }
385
+ }
386
+ }
387
+ ```
388
+
389
+ This allows for fine-grained control over which headers are applied based on the request method, useful for implementing different caching strategies for read vs. write operations.
390
+
391
+ #### Content-Security-Policy and security considerations
392
+
393
+ With the default CSP your html application will not work, to make it working without impacting the security Of the other pages you should add in the `headers_config` something like this:
394
+
395
+ ```python
396
+ {
397
+ "my_page": {
398
+ "path_match": r"^your-path/?",
399
+ "headers": {
400
+ "Content-Security-Policy": {
401
+ "default-src": ["'self'"],
402
+ "script-src-elem": ["'self'", ...],
403
+ "style-src-elem": ["'self'", ...],
404
+ }
405
+ },
406
+ "order": 1
407
+ }
408
+ }
409
+ ```
410
+
411
+ And do the same for other headers.
412
+
413
+ #### Cache-Control Header
414
+
415
+ The `Cache-Control` header can be configured to control caching behavior for different endpoints. You can specify it as a string, list, or dictionary:
416
+
417
+ ```python
418
+ {
419
+ "api_endpoints": {
420
+ "path_match": r"^/api/.*",
421
+ "headers": {
422
+ "Cache-Control": ["public", "max-age=3600"] # Cache for 1 hour
423
+ },
424
+ "order": 1
425
+ }
426
+ }
427
+ ```
428
+
429
+ By default the middleware will not set any `Cache-Control` header, so you should explicitly configure it to enable caching.
430
+
431
+ ## Authentication
432
+
433
+ The package also provides authentication utilities for GitHub-based authentication and API key validation. See the `auth.py` module for detailed configuration options.
434
+
435
+ ## Prometheus Metrics
436
+
437
+ To enable Prometheus metrics in your FastAPI application, you can use the `prometheus_fastapi_instrumentator` package. Here's how to set it up:
438
+
439
+ ```python
440
+ from c2casgiutils import config
441
+ from prometheus_client import start_http_server
442
+ from prometheus_fastapi_instrumentator import Instrumentator
443
+
444
+ # Get Prometheus HTTP server port from environment variable 9000 by default
445
+ start_http_server(config.settings.prometheus.port)
446
+
447
+ instrumentator = Instrumentator(should_instrument_requests_inprogress=True)
448
+ instrumentator.instrument(app)
449
+ ```
450
+
451
+ ## Sentry Integration
452
+
453
+ To enable error tracking with Sentry in your application:
454
+
455
+ ```python
456
+ import os
457
+ import sentry_sdk
458
+
459
+ # Initialize Sentry if the URL is provided
460
+ if config.settings.sentry.dsn or 'SENTRY_DSN' in os.environ:
461
+ _LOGGER.info("Sentry is enabled with URL: %s", config.settings.sentry.url or os.environ.get("SENTRY_DSN"))
462
+ sentry_sdk.init(**config.settings.sentry.model_dump())
463
+ ```
464
+
465
+ Sentry will automatically capture exceptions and errors in your FastAPI application. For more advanced usage, refer to the [Sentry Python SDK documentation](https://docs.sentry.io/platforms/python/) and [FastAPI integration guide](https://docs.sentry.io/platforms/python/integrations/fastapi/).
466
+