mdb-engine 0.4.6__py3-none-any.whl → 0.4.9__py3-none-any.whl
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.
- mdb_engine/__init__.py +5 -2
- mdb_engine/auth/csrf.py +44 -3
- mdb_engine/core/engine.py +149 -2
- {mdb_engine-0.4.6.dist-info → mdb_engine-0.4.9.dist-info}/METADATA +1 -1
- {mdb_engine-0.4.6.dist-info → mdb_engine-0.4.9.dist-info}/RECORD +9 -9
- {mdb_engine-0.4.6.dist-info → mdb_engine-0.4.9.dist-info}/WHEEL +0 -0
- {mdb_engine-0.4.6.dist-info → mdb_engine-0.4.9.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.4.6.dist-info → mdb_engine-0.4.9.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.4.6.dist-info → mdb_engine-0.4.9.dist-info}/top_level.txt +0 -0
mdb_engine/__init__.py
CHANGED
|
@@ -82,8 +82,11 @@ from .repositories import Entity, MongoRepository, Repository, UnitOfWork
|
|
|
82
82
|
from .utils import clean_mongo_doc, clean_mongo_docs
|
|
83
83
|
|
|
84
84
|
__version__ = (
|
|
85
|
-
"0.4.
|
|
86
|
-
#
|
|
85
|
+
"0.4.9" # Fix: WebSocket + CSRF + Multi-App architecture
|
|
86
|
+
# - CSRF middleware now added to parent app when child apps use shared auth
|
|
87
|
+
# - CORS config properly merged from child apps to parent app
|
|
88
|
+
# - WebSocket origin validation uses parent app's CORS config
|
|
89
|
+
# - Comprehensive integration tests added
|
|
87
90
|
)
|
|
88
91
|
|
|
89
92
|
__all__ = [
|
mdb_engine/auth/csrf.py
CHANGED
|
@@ -190,6 +190,8 @@ class CSRFMiddleware(BaseHTTPMiddleware):
|
|
|
190
190
|
|
|
191
191
|
def _is_exempt(self, path: str) -> bool:
|
|
192
192
|
"""Check if a path is exempt from CSRF validation."""
|
|
193
|
+
# WebSocket upgrade requests are handled separately in dispatch()
|
|
194
|
+
# Don't exempt them here - they need origin validation
|
|
193
195
|
for pattern in self.exempt_routes:
|
|
194
196
|
if fnmatch.fnmatch(path, pattern):
|
|
195
197
|
return True
|
|
@@ -201,14 +203,42 @@ class CSRFMiddleware(BaseHTTPMiddleware):
|
|
|
201
203
|
return upgrade_header == "websocket"
|
|
202
204
|
|
|
203
205
|
def _get_allowed_origins(self, request: Request) -> list[str]:
|
|
204
|
-
"""
|
|
206
|
+
"""
|
|
207
|
+
Get allowed origins from app state (CORS config) or use request host as fallback.
|
|
208
|
+
|
|
209
|
+
For multi-app setups, checks parent app's CORS config first (since WebSocket routes
|
|
210
|
+
are registered on parent app), then falls back to request host.
|
|
211
|
+
"""
|
|
205
212
|
try:
|
|
213
|
+
# Check current app's CORS config (parent app for WebSocket routes in multi-app)
|
|
206
214
|
cors_config = getattr(request.app.state, "cors_config", None)
|
|
207
215
|
if cors_config and cors_config.get("allow_origins"):
|
|
208
|
-
|
|
216
|
+
origins = cors_config["allow_origins"]
|
|
217
|
+
if origins:
|
|
218
|
+
return origins if isinstance(origins, list) else [origins]
|
|
209
219
|
except (AttributeError, TypeError, KeyError):
|
|
210
220
|
pass
|
|
211
221
|
|
|
222
|
+
# Fallback: Check if this is a multi-app setup and try to find mounted app's CORS config
|
|
223
|
+
try:
|
|
224
|
+
if hasattr(request.app.state, "mounted_apps"):
|
|
225
|
+
# This is a parent app in multi-app setup
|
|
226
|
+
# Try to find which mounted app this request is for
|
|
227
|
+
path = request.url.path
|
|
228
|
+
mounted_apps = request.app.state.mounted_apps
|
|
229
|
+
|
|
230
|
+
# Find matching mounted app by path prefix
|
|
231
|
+
for app_info in mounted_apps:
|
|
232
|
+
path_prefix = app_info.get("path_prefix", "")
|
|
233
|
+
if path_prefix and path.startswith(path_prefix):
|
|
234
|
+
# Try to get child app's CORS config if available
|
|
235
|
+
# Note: Child app might not be directly accessible, so we rely on
|
|
236
|
+
# parent app's merged CORS config (set during mounting)
|
|
237
|
+
break
|
|
238
|
+
except (AttributeError, TypeError, KeyError):
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
# Final fallback: Use request host
|
|
212
242
|
try:
|
|
213
243
|
host = request.url.hostname
|
|
214
244
|
scheme = request.url.scheme
|
|
@@ -247,7 +277,8 @@ class CSRFMiddleware(BaseHTTPMiddleware):
|
|
|
247
277
|
|
|
248
278
|
logger.warning(
|
|
249
279
|
f"WebSocket upgrade rejected - invalid Origin: {origin} "
|
|
250
|
-
f"(allowed: {allowed_origins})"
|
|
280
|
+
f"(allowed: {allowed_origins}, app: {getattr(request.app, 'title', 'unknown')}, "
|
|
281
|
+
f"has_cors_config: {hasattr(request.app.state, 'cors_config')})"
|
|
251
282
|
)
|
|
252
283
|
return False
|
|
253
284
|
|
|
@@ -262,12 +293,22 @@ class CSRFMiddleware(BaseHTTPMiddleware):
|
|
|
262
293
|
path = request.url.path
|
|
263
294
|
method = request.method
|
|
264
295
|
|
|
296
|
+
# CRITICAL: Handle WebSocket upgrade requests BEFORE other CSRF checks
|
|
297
|
+
# WebSocket upgrades don't use CSRF tokens, but need origin validation
|
|
265
298
|
if self._is_websocket_upgrade(request):
|
|
299
|
+
# Validate origin for WebSocket connections (CSWSH protection)
|
|
266
300
|
if not self._validate_websocket_origin(request):
|
|
301
|
+
logger.warning(
|
|
302
|
+
f"WebSocket origin validation failed for {path}: "
|
|
303
|
+
f"origin={request.headers.get('origin')}, "
|
|
304
|
+
f"allowed={self._get_allowed_origins(request)}"
|
|
305
|
+
)
|
|
267
306
|
return JSONResponse(
|
|
268
307
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
269
308
|
content={"detail": "Invalid origin for WebSocket connection"},
|
|
270
309
|
)
|
|
310
|
+
# Origin validated - allow WebSocket upgrade to proceed
|
|
311
|
+
# No CSRF token check needed for WebSocket upgrades
|
|
271
312
|
return await call_next(request)
|
|
272
313
|
|
|
273
314
|
if self._is_exempt(path):
|
mdb_engine/core/engine.py
CHANGED
|
@@ -2179,6 +2179,116 @@ class MongoDBEngine:
|
|
|
2179
2179
|
return entry
|
|
2180
2180
|
return None
|
|
2181
2181
|
|
|
2182
|
+
async def _merge_cors_config_to_parent(
|
|
2183
|
+
parent_app: "FastAPI",
|
|
2184
|
+
child_app: "FastAPI",
|
|
2185
|
+
child_manifest: dict[str, Any],
|
|
2186
|
+
slug: str,
|
|
2187
|
+
) -> None:
|
|
2188
|
+
"""Merge CORS config from child app to parent app."""
|
|
2189
|
+
child_cors = None
|
|
2190
|
+
if hasattr(child_app.state, "cors_config"):
|
|
2191
|
+
child_cors = child_app.state.cors_config
|
|
2192
|
+
else:
|
|
2193
|
+
# CORS config might not be set yet (lifespan runs asynchronously)
|
|
2194
|
+
# Get it from manifest directly
|
|
2195
|
+
cors_config_from_manifest = child_manifest.get("cors", {})
|
|
2196
|
+
if cors_config_from_manifest:
|
|
2197
|
+
from ..auth.config_helpers import (
|
|
2198
|
+
CORS_DEFAULTS,
|
|
2199
|
+
merge_config_with_defaults,
|
|
2200
|
+
)
|
|
2201
|
+
|
|
2202
|
+
child_cors = merge_config_with_defaults(
|
|
2203
|
+
cors_config_from_manifest, CORS_DEFAULTS
|
|
2204
|
+
)
|
|
2205
|
+
# Also set it on child app state for future reference
|
|
2206
|
+
child_app.state.cors_config = child_cors
|
|
2207
|
+
|
|
2208
|
+
if child_cors:
|
|
2209
|
+
if hasattr(parent_app.state, "cors_config"):
|
|
2210
|
+
# Merge child CORS into parent (child takes precedence for its routes)
|
|
2211
|
+
parent_cors = parent_app.state.cors_config
|
|
2212
|
+
# Merge allow_origins lists
|
|
2213
|
+
child_origins = child_cors.get("allow_origins", [])
|
|
2214
|
+
parent_origins = parent_cors.get("allow_origins", [])
|
|
2215
|
+
merged_origins = list(set(parent_origins + child_origins))
|
|
2216
|
+
parent_app.state.cors_config = {
|
|
2217
|
+
**parent_cors,
|
|
2218
|
+
**child_cors,
|
|
2219
|
+
"allow_origins": merged_origins if merged_origins else ["*"],
|
|
2220
|
+
}
|
|
2221
|
+
else:
|
|
2222
|
+
# Parent has no CORS config, use child's
|
|
2223
|
+
parent_app.state.cors_config = child_cors
|
|
2224
|
+
logger.debug(f"✅ Merged CORS config from child app '{slug}' to parent app")
|
|
2225
|
+
|
|
2226
|
+
async def _register_websocket_routes(
|
|
2227
|
+
parent_app: "FastAPI",
|
|
2228
|
+
child_manifest: dict[str, Any],
|
|
2229
|
+
slug: str,
|
|
2230
|
+
path_prefix: str,
|
|
2231
|
+
) -> None:
|
|
2232
|
+
"""Register WebSocket routes on parent app for a child app."""
|
|
2233
|
+
websockets_config = child_manifest.get("websockets")
|
|
2234
|
+
if not websockets_config:
|
|
2235
|
+
return
|
|
2236
|
+
|
|
2237
|
+
try:
|
|
2238
|
+
from fastapi import APIRouter
|
|
2239
|
+
|
|
2240
|
+
from ..routing.websockets import create_websocket_endpoint
|
|
2241
|
+
|
|
2242
|
+
for endpoint_name, endpoint_config in websockets_config.items():
|
|
2243
|
+
ws_path = endpoint_config.get("path", f"/{endpoint_name}")
|
|
2244
|
+
# Combine mount prefix with WebSocket path
|
|
2245
|
+
full_ws_path = f"{path_prefix.rstrip('/')}{ws_path}"
|
|
2246
|
+
|
|
2247
|
+
# Handle auth configuration
|
|
2248
|
+
auth_config = endpoint_config.get("auth", {})
|
|
2249
|
+
if isinstance(auth_config, dict) and "required" in auth_config:
|
|
2250
|
+
require_auth = auth_config.get("required", True)
|
|
2251
|
+
elif "require_auth" in endpoint_config:
|
|
2252
|
+
require_auth = endpoint_config.get("require_auth", True)
|
|
2253
|
+
else:
|
|
2254
|
+
# Use app's auth_policy if available
|
|
2255
|
+
if "auth_policy" in child_manifest:
|
|
2256
|
+
require_auth = child_manifest["auth_policy"].get("required", True)
|
|
2257
|
+
else:
|
|
2258
|
+
require_auth = True
|
|
2259
|
+
|
|
2260
|
+
ping_interval = endpoint_config.get("ping_interval", 30)
|
|
2261
|
+
|
|
2262
|
+
# Create WebSocket handler
|
|
2263
|
+
handler = create_websocket_endpoint(
|
|
2264
|
+
app_slug=slug,
|
|
2265
|
+
path=ws_path,
|
|
2266
|
+
endpoint_name=endpoint_name,
|
|
2267
|
+
handler=None,
|
|
2268
|
+
require_auth=require_auth,
|
|
2269
|
+
ping_interval=ping_interval,
|
|
2270
|
+
)
|
|
2271
|
+
|
|
2272
|
+
# Register on parent app with full path
|
|
2273
|
+
ws_router = APIRouter()
|
|
2274
|
+
ws_router.websocket(full_ws_path)(handler)
|
|
2275
|
+
parent_app.include_router(ws_router)
|
|
2276
|
+
|
|
2277
|
+
logger.info(
|
|
2278
|
+
f"✅ Registered WebSocket route '{full_ws_path}' "
|
|
2279
|
+
f"for mounted app '{slug}' (mounted at '{path_prefix}')"
|
|
2280
|
+
)
|
|
2281
|
+
except ImportError:
|
|
2282
|
+
logger.warning(
|
|
2283
|
+
f"WebSocket support not available - skipping WebSocket routes "
|
|
2284
|
+
f"for mounted app '{slug}'"
|
|
2285
|
+
)
|
|
2286
|
+
except (ValueError, TypeError, AttributeError, RuntimeError) as e:
|
|
2287
|
+
logger.error(
|
|
2288
|
+
f"Failed to register WebSocket routes for mounted app '{slug}': {e}",
|
|
2289
|
+
exc_info=True,
|
|
2290
|
+
)
|
|
2291
|
+
|
|
2182
2292
|
@asynccontextmanager
|
|
2183
2293
|
async def lifespan(app: FastAPI):
|
|
2184
2294
|
"""Lifespan context manager for parent app."""
|
|
@@ -2375,6 +2485,13 @@ class MongoDBEngine:
|
|
|
2375
2485
|
|
|
2376
2486
|
# Mount child app at path prefix
|
|
2377
2487
|
app.mount(path_prefix, child_app)
|
|
2488
|
+
|
|
2489
|
+
# CRITICAL FIX: Merge CORS config from child app to parent app
|
|
2490
|
+
await _merge_cors_config_to_parent(app, child_app, app_manifest_data, slug)
|
|
2491
|
+
|
|
2492
|
+
# CRITICAL FIX: Register WebSocket routes on parent app with full path
|
|
2493
|
+
await _register_websocket_routes(app, app_manifest_data, slug, path_prefix)
|
|
2494
|
+
|
|
2378
2495
|
# Update existing entry instead of appending
|
|
2379
2496
|
entry = _find_mounted_app_entry(slug)
|
|
2380
2497
|
if entry:
|
|
@@ -2550,6 +2667,16 @@ class MongoDBEngine:
|
|
|
2550
2667
|
parent_app.state.is_multi_app = True
|
|
2551
2668
|
parent_app.state.engine = engine
|
|
2552
2669
|
|
|
2670
|
+
# Set default CORS config on parent app for WebSocket origin validation
|
|
2671
|
+
# This ensures CSRF middleware can validate WebSocket origins even if child apps
|
|
2672
|
+
# don't configure CORS
|
|
2673
|
+
from ..auth.config_helpers import CORS_DEFAULTS
|
|
2674
|
+
|
|
2675
|
+
parent_app.state.cors_config = CORS_DEFAULTS.copy()
|
|
2676
|
+
parent_app.state.cors_config["enabled"] = True
|
|
2677
|
+
parent_app.state.cors_config["allow_origins"] = ["*"] # Default to allow all for WebSocket
|
|
2678
|
+
logger.debug("Set default CORS config on parent app for WebSocket origin validation")
|
|
2679
|
+
|
|
2553
2680
|
# Store app reference in engine for get_mounted_apps()
|
|
2554
2681
|
engine._multi_app_instance = parent_app
|
|
2555
2682
|
|
|
@@ -2572,15 +2699,35 @@ class MongoDBEngine:
|
|
|
2572
2699
|
parent_app.add_middleware(RequestScopeMiddleware)
|
|
2573
2700
|
logger.debug("RequestScopeMiddleware added for parent app")
|
|
2574
2701
|
|
|
2702
|
+
# CRITICAL: Add CSRF middleware to parent app if any child app uses shared auth
|
|
2703
|
+
# WebSocket routes are registered on parent app, so parent app middleware runs first
|
|
2704
|
+
# CSRF middleware on parent app validates WebSocket origin using parent app's CORS config
|
|
2705
|
+
if has_shared_auth:
|
|
2706
|
+
from ..auth.csrf import create_csrf_middleware
|
|
2707
|
+
|
|
2708
|
+
# Create CSRF middleware with default config (will use parent app's CORS config)
|
|
2709
|
+
# Exempt routes that don't need CSRF (health checks, etc.)
|
|
2710
|
+
parent_csrf_config = {
|
|
2711
|
+
"csrf_protection": True,
|
|
2712
|
+
"public_routes": ["/health", "/docs", "/openapi.json", "/_mdb/routes"],
|
|
2713
|
+
}
|
|
2714
|
+
csrf_middleware = create_csrf_middleware(parent_csrf_config)
|
|
2715
|
+
parent_app.add_middleware(csrf_middleware)
|
|
2716
|
+
logger.info("CSRFMiddleware added to parent app for WebSocket origin validation")
|
|
2717
|
+
|
|
2575
2718
|
# Add shared CORS middleware if configured
|
|
2576
2719
|
# (Individual apps can add their own CORS, but parent-level is useful)
|
|
2577
2720
|
try:
|
|
2578
2721
|
from fastapi.middleware.cors import CORSMiddleware
|
|
2579
2722
|
|
|
2723
|
+
# Use CORS config from parent app state (set above)
|
|
2724
|
+
cors_origins = parent_app.state.cors_config.get("allow_origins", ["*"])
|
|
2725
|
+
cors_credentials = parent_app.state.cors_config.get("allow_credentials", True)
|
|
2726
|
+
|
|
2580
2727
|
parent_app.add_middleware(
|
|
2581
2728
|
CORSMiddleware,
|
|
2582
|
-
allow_origins=
|
|
2583
|
-
allow_credentials=
|
|
2729
|
+
allow_origins=cors_origins,
|
|
2730
|
+
allow_credentials=cors_credentials,
|
|
2584
2731
|
allow_methods=["*"],
|
|
2585
2732
|
allow_headers=["*"],
|
|
2586
2733
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
mdb_engine/README.md,sha256=T3EFGcPopY9LslYW3lxgG3hohWkAOmBNbYG0FDMUJiY,3502
|
|
2
|
-
mdb_engine/__init__.py,sha256=
|
|
2
|
+
mdb_engine/__init__.py,sha256=AFQ20lpN4UqRakhVC_JPQzqJkvLAbwgYbgCnFBaYet4,3413
|
|
3
3
|
mdb_engine/config.py,sha256=DTAyxfKB8ogyI0v5QR9Y-SJOgXQr_eDBCKxNBSqEyLc,7269
|
|
4
4
|
mdb_engine/constants.py,sha256=eaotvW57TVOg7rRbLziGrVNoP7adgw_G9iVByHezc_A,7837
|
|
5
5
|
mdb_engine/dependencies.py,sha256=MJuYQhZ9ZGzXlip1ha5zba9Rvn04HDPWahJFJH81Q2s,14107
|
|
@@ -14,7 +14,7 @@ mdb_engine/auth/casbin_models.py,sha256=7XtFmRBhhjw1nKprnluvjyJoTj5fzdPeQwVvo6fI
|
|
|
14
14
|
mdb_engine/auth/config_defaults.py,sha256=1YI_hIHuTiEXpkEYMcufNHdLr1oxPiJylg3CKrJCSGY,2012
|
|
15
15
|
mdb_engine/auth/config_helpers.py,sha256=Qharb2YagLOKDGtE7XhYRDbBoQ_KGykrcIKrsOwWIJ4,6303
|
|
16
16
|
mdb_engine/auth/cookie_utils.py,sha256=j04qXq5GiJrnnJUAP5Z_N1CAFbx9CZiyF5u9xIiQ3vo,4876
|
|
17
|
-
mdb_engine/auth/csrf.py,sha256=
|
|
17
|
+
mdb_engine/auth/csrf.py,sha256=__vbWkqt6LcEoib53q14Or7hGkG7yKC2UZdbf8Si3h4,17211
|
|
18
18
|
mdb_engine/auth/decorators.py,sha256=LkVVEuRrT0Iz8EwctN14BEi3fSV-xtN6DaGXgtbiYYo,12287
|
|
19
19
|
mdb_engine/auth/dependencies.py,sha256=JB1iYvZJgTR6gcaiGe_GJFCS6NdUKMxWBZRv6vVxnzw,27112
|
|
20
20
|
mdb_engine/auth/helpers.py,sha256=BCrid985cYh-3h5ZMUV9TES0q40uJXio4oYKQZta7KA,1970
|
|
@@ -46,7 +46,7 @@ mdb_engine/core/app_registration.py,sha256=7szt2a7aBkpSppjmhdkkPPYMKGKo0MkLKZeEe
|
|
|
46
46
|
mdb_engine/core/app_secrets.py,sha256=bo-syg9UUATibNyXEZs-0TTYWG-JaY-2S0yNSGA12n0,10524
|
|
47
47
|
mdb_engine/core/connection.py,sha256=XnwuPG34pJ7kJGJ84T0mhj1UZ6_CLz_9qZf6NRYGIS8,8346
|
|
48
48
|
mdb_engine/core/encryption.py,sha256=RZ5LPF5g28E3ZBn6v1IMw_oas7u9YGFtBcEj8lTi9LM,7515
|
|
49
|
-
mdb_engine/core/engine.py,sha256=
|
|
49
|
+
mdb_engine/core/engine.py,sha256=1coGBNfC1f-tFV5mlRMN9WBkk3IkV3atH36dSUZyETQ,140964
|
|
50
50
|
mdb_engine/core/index_management.py,sha256=9-r7MIy3JnjQ35sGqsbj8K_I07vAUWtAVgSWC99lJcE,5555
|
|
51
51
|
mdb_engine/core/manifest.py,sha256=jguhjVPAHMZGxOJcdSGouv9_XiKmxUjDmyjn2yXHCj4,139205
|
|
52
52
|
mdb_engine/core/ray_integration.py,sha256=vexYOzztscvRYje1xTNmXJbi99oJxCaVJAwKfTNTF_E,13610
|
|
@@ -89,9 +89,9 @@ mdb_engine/routing/__init__.py,sha256=reupjHi_RTc2ZBA4AH5XzobAmqy4EQIsfSUcTkFknU
|
|
|
89
89
|
mdb_engine/routing/websockets.py,sha256=3X4OjQv_Nln4UmeifJky0gFhMG8A6alR77I8g1iIOLY,29311
|
|
90
90
|
mdb_engine/utils/__init__.py,sha256=lDxQSGqkV4fVw5TWIk6FA6_eey_ZnEtMY0fir3cpAe8,236
|
|
91
91
|
mdb_engine/utils/mongo.py,sha256=Oqtv4tQdpiiZzrilGLEYQPo8Vmh8WsTQypxQs8Of53s,3369
|
|
92
|
-
mdb_engine-0.4.
|
|
93
|
-
mdb_engine-0.4.
|
|
94
|
-
mdb_engine-0.4.
|
|
95
|
-
mdb_engine-0.4.
|
|
96
|
-
mdb_engine-0.4.
|
|
97
|
-
mdb_engine-0.4.
|
|
92
|
+
mdb_engine-0.4.9.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
93
|
+
mdb_engine-0.4.9.dist-info/METADATA,sha256=ncHUSb3JIKOLV1bUrn1BZWoMR-qUg2QEvmWQ0dzFlnM,15810
|
|
94
|
+
mdb_engine-0.4.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
95
|
+
mdb_engine-0.4.9.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
|
|
96
|
+
mdb_engine-0.4.9.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
|
|
97
|
+
mdb_engine-0.4.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|