tachyon-api 0.9.0__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.
Files changed (44) hide show
  1. tachyon_api/__init__.py +59 -0
  2. tachyon_api/app.py +699 -0
  3. tachyon_api/background.py +72 -0
  4. tachyon_api/cache.py +270 -0
  5. tachyon_api/cli/__init__.py +9 -0
  6. tachyon_api/cli/__main__.py +8 -0
  7. tachyon_api/cli/commands/__init__.py +5 -0
  8. tachyon_api/cli/commands/generate.py +190 -0
  9. tachyon_api/cli/commands/lint.py +186 -0
  10. tachyon_api/cli/commands/new.py +82 -0
  11. tachyon_api/cli/commands/openapi.py +128 -0
  12. tachyon_api/cli/main.py +69 -0
  13. tachyon_api/cli/templates/__init__.py +8 -0
  14. tachyon_api/cli/templates/project.py +194 -0
  15. tachyon_api/cli/templates/service.py +330 -0
  16. tachyon_api/core/__init__.py +12 -0
  17. tachyon_api/core/lifecycle.py +106 -0
  18. tachyon_api/core/websocket.py +92 -0
  19. tachyon_api/di.py +86 -0
  20. tachyon_api/exceptions.py +39 -0
  21. tachyon_api/files.py +14 -0
  22. tachyon_api/middlewares/__init__.py +4 -0
  23. tachyon_api/middlewares/core.py +40 -0
  24. tachyon_api/middlewares/cors.py +159 -0
  25. tachyon_api/middlewares/logger.py +123 -0
  26. tachyon_api/models.py +73 -0
  27. tachyon_api/openapi.py +419 -0
  28. tachyon_api/params.py +268 -0
  29. tachyon_api/processing/__init__.py +14 -0
  30. tachyon_api/processing/dependencies.py +172 -0
  31. tachyon_api/processing/parameters.py +484 -0
  32. tachyon_api/processing/response_processor.py +93 -0
  33. tachyon_api/responses.py +92 -0
  34. tachyon_api/router.py +161 -0
  35. tachyon_api/security.py +295 -0
  36. tachyon_api/testing.py +110 -0
  37. tachyon_api/utils/__init__.py +15 -0
  38. tachyon_api/utils/type_converter.py +113 -0
  39. tachyon_api/utils/type_utils.py +162 -0
  40. tachyon_api-0.9.0.dist-info/METADATA +291 -0
  41. tachyon_api-0.9.0.dist-info/RECORD +44 -0
  42. tachyon_api-0.9.0.dist-info/WHEEL +4 -0
  43. tachyon_api-0.9.0.dist-info/entry_points.txt +3 -0
  44. tachyon_api-0.9.0.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,186 @@
1
+ """
2
+ tachyon lint - Code quality tools (ruff wrapper)
3
+ """
4
+
5
+ import typer
6
+ import subprocess
7
+ import shutil
8
+ from pathlib import Path
9
+ from typing import Optional, List
10
+
11
+ app = typer.Typer(no_args_is_help=True)
12
+
13
+
14
+ def _check_ruff_installed() -> bool:
15
+ """Check if ruff is installed."""
16
+ return shutil.which("ruff") is not None
17
+
18
+
19
+ def _run_ruff(args: List[str], check: bool = True) -> int:
20
+ """Run ruff with given arguments."""
21
+ if not _check_ruff_installed():
22
+ typer.secho(
23
+ "āŒ ruff is not installed. Install it with: pip install ruff",
24
+ fg=typer.colors.RED,
25
+ )
26
+ raise typer.Exit(1)
27
+
28
+ cmd = ["ruff"] + args
29
+ result = subprocess.run(cmd)
30
+ return result.returncode
31
+
32
+
33
+ @app.command()
34
+ def check(
35
+ path: Optional[Path] = typer.Argument(
36
+ None, help="Path to check (default: current directory)"
37
+ ),
38
+ fix: bool = typer.Option(
39
+ False, "--fix", "-f", help="Automatically fix issues where possible"
40
+ ),
41
+ watch: bool = typer.Option(
42
+ False, "--watch", "-w", help="Watch for changes and re-run"
43
+ ),
44
+ ):
45
+ """
46
+ šŸ” Check code for linting issues.
47
+
48
+ Example:
49
+ tachyon lint check
50
+ tachyon lint check --fix
51
+ tachyon lint check ./src --watch
52
+ """
53
+ target = str(path) if path else "."
54
+
55
+ args = ["check", target]
56
+
57
+ if fix:
58
+ args.append("--fix")
59
+
60
+ if watch:
61
+ args.append("--watch")
62
+
63
+ typer.echo(f"šŸ” Checking: {target}\n")
64
+ exit_code = _run_ruff(args)
65
+
66
+ if exit_code == 0:
67
+ typer.secho("\nāœ… No issues found!", fg=typer.colors.GREEN)
68
+
69
+ raise typer.Exit(exit_code)
70
+
71
+
72
+ @app.command()
73
+ def fix(
74
+ path: Optional[Path] = typer.Argument(
75
+ None, help="Path to fix (default: current directory)"
76
+ ),
77
+ unsafe: bool = typer.Option(False, "--unsafe", help="Apply unsafe fixes as well"),
78
+ ):
79
+ """
80
+ šŸ”§ Automatically fix linting issues.
81
+
82
+ Example:
83
+ tachyon lint fix
84
+ tachyon lint fix ./src
85
+ tachyon lint fix --unsafe
86
+ """
87
+ target = str(path) if path else "."
88
+
89
+ args = ["check", target, "--fix"]
90
+
91
+ if unsafe:
92
+ args.append("--unsafe-fixes")
93
+
94
+ typer.echo(f"šŸ”§ Fixing: {target}\n")
95
+ exit_code = _run_ruff(args)
96
+
97
+ if exit_code == 0:
98
+ typer.secho("\nāœ… All fixable issues resolved!", fg=typer.colors.GREEN)
99
+
100
+ raise typer.Exit(exit_code)
101
+
102
+
103
+ @app.command()
104
+ def format(
105
+ path: Optional[Path] = typer.Argument(
106
+ None, help="Path to format (default: current directory)"
107
+ ),
108
+ check_only: bool = typer.Option(
109
+ False, "--check", help="Check formatting without making changes"
110
+ ),
111
+ diff: bool = typer.Option(False, "--diff", help="Show diff of formatting changes"),
112
+ ):
113
+ """
114
+ šŸŽØ Format code using ruff formatter.
115
+
116
+ Example:
117
+ tachyon lint format
118
+ tachyon lint format --check
119
+ tachyon lint format --diff
120
+ """
121
+ target = str(path) if path else "."
122
+
123
+ args = ["format", target]
124
+
125
+ if check_only:
126
+ args.append("--check")
127
+
128
+ if diff:
129
+ args.append("--diff")
130
+
131
+ typer.echo(f"šŸŽØ Formatting: {target}\n")
132
+ exit_code = _run_ruff(args)
133
+
134
+ if exit_code == 0 and not check_only:
135
+ typer.secho("\nāœ… Formatting complete!", fg=typer.colors.GREEN)
136
+ elif exit_code == 0 and check_only:
137
+ typer.secho("\nāœ… Code is properly formatted!", fg=typer.colors.GREEN)
138
+
139
+ raise typer.Exit(exit_code)
140
+
141
+
142
+ @app.command()
143
+ def all(
144
+ path: Optional[Path] = typer.Argument(
145
+ None, help="Path to check and format (default: current directory)"
146
+ ),
147
+ fix: bool = typer.Option(True, "--fix/--no-fix", help="Auto-fix issues"),
148
+ ):
149
+ """
150
+ šŸš€ Run all quality checks: lint + format.
151
+
152
+ Example:
153
+ tachyon lint all
154
+ tachyon lint all --no-fix
155
+ """
156
+ target = str(path) if path else "."
157
+
158
+ typer.echo(f"šŸš€ Running all quality checks on: {target}\n")
159
+
160
+ # Run linting
161
+ typer.echo("─" * 40)
162
+ typer.echo("šŸ” Step 1: Linting")
163
+ typer.echo("─" * 40)
164
+
165
+ lint_args = ["check", target]
166
+ if fix:
167
+ lint_args.append("--fix")
168
+
169
+ lint_code = _run_ruff(lint_args)
170
+
171
+ # Run formatting
172
+ typer.echo("\n" + "─" * 40)
173
+ typer.echo("šŸŽØ Step 2: Formatting")
174
+ typer.echo("─" * 40)
175
+
176
+ format_args = ["format", target]
177
+ format_code = _run_ruff(format_args)
178
+
179
+ # Summary
180
+ typer.echo("\n" + "═" * 40)
181
+ if lint_code == 0 and format_code == 0:
182
+ typer.secho("āœ… All checks passed!", fg=typer.colors.GREEN, bold=True)
183
+ else:
184
+ typer.secho("āš ļø Some issues found", fg=typer.colors.YELLOW, bold=True)
185
+
186
+ raise typer.Exit(max(lint_code, format_code))
@@ -0,0 +1,82 @@
1
+ """
2
+ tachyon new - Create new project with clean architecture
3
+ """
4
+
5
+ import typer
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from ..templates import ProjectTemplates
10
+
11
+
12
+ def create_project(name: str, parent_path: Optional[Path] = None):
13
+ """
14
+ Create a new Tachyon project with clean architecture structure.
15
+
16
+ Structure:
17
+ my-api/
18
+ ā”œā”€ā”€ app.py
19
+ ā”œā”€ā”€ config.py
20
+ ā”œā”€ā”€ requirements.txt
21
+ ā”œā”€ā”€ modules/
22
+ │ └── __init__.py
23
+ ā”œā”€ā”€ shared/
24
+ │ ā”œā”€ā”€ __init__.py
25
+ │ ā”œā”€ā”€ exceptions.py
26
+ │ └── dependencies.py
27
+ └── tests/
28
+ ā”œā”€ā”€ __init__.py
29
+ └── conftest.py
30
+ """
31
+ # Determine project path
32
+ base_path = parent_path or Path.cwd()
33
+ project_path = base_path / name
34
+
35
+ # Check if already exists
36
+ if project_path.exists():
37
+ typer.secho(f"āŒ Directory '{name}' already exists!", fg=typer.colors.RED)
38
+ raise typer.Exit(1)
39
+
40
+ typer.echo(f"\nšŸš€ Creating Tachyon project: {typer.style(name, bold=True)}\n")
41
+
42
+ # Create directory structure
43
+ directories = [
44
+ project_path,
45
+ project_path / "modules",
46
+ project_path / "shared",
47
+ project_path / "tests",
48
+ ]
49
+
50
+ for directory in directories:
51
+ directory.mkdir(parents=True, exist_ok=True)
52
+ typer.echo(f" šŸ“ Created {directory.relative_to(base_path)}/")
53
+
54
+ # Create files
55
+ files = {
56
+ "app.py": ProjectTemplates.APP,
57
+ "config.py": ProjectTemplates.CONFIG,
58
+ "requirements.txt": ProjectTemplates.REQUIREMENTS,
59
+ "modules/__init__.py": ProjectTemplates.MODULES_INIT,
60
+ "shared/__init__.py": ProjectTemplates.SHARED_INIT,
61
+ "shared/exceptions.py": ProjectTemplates.SHARED_EXCEPTIONS,
62
+ "shared/dependencies.py": ProjectTemplates.SHARED_DEPENDENCIES,
63
+ "tests/__init__.py": "",
64
+ "tests/conftest.py": ProjectTemplates.TESTS_CONFTEST,
65
+ }
66
+
67
+ for file_path, content in files.items():
68
+ full_path = project_path / file_path
69
+ full_path.write_text(content)
70
+ typer.echo(f" šŸ“„ Created {file_path}")
71
+
72
+ # Success message
73
+ typer.echo(
74
+ f"\nāœ… Project {typer.style(name, bold=True, fg=typer.colors.GREEN)} created successfully!"
75
+ )
76
+ typer.echo("\nšŸ“– Next steps:")
77
+ typer.echo(f" cd {name}")
78
+ typer.echo(" pip install -r requirements.txt")
79
+ typer.echo(" python app.py")
80
+ typer.echo("\n Then generate your first service:")
81
+ typer.echo(" tachyon g service users")
82
+ typer.echo()
@@ -0,0 +1,128 @@
1
+ """
2
+ tachyon openapi - OpenAPI schema utilities
3
+ """
4
+
5
+ import typer
6
+ import json
7
+ import importlib
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ app = typer.Typer(no_args_is_help=True)
12
+
13
+
14
+ def _load_app(app_path: str):
15
+ """
16
+ Load a Tachyon app from module:attribute format.
17
+
18
+ Example: "app:app" or "main:application"
19
+ """
20
+ try:
21
+ module_path, attr_name = app_path.split(":")
22
+ except ValueError:
23
+ typer.secho(
24
+ "āŒ Invalid app path format. Use 'module:attribute' (e.g., 'app:app')",
25
+ fg=typer.colors.RED,
26
+ )
27
+ raise typer.Exit(1)
28
+
29
+ try:
30
+ module = importlib.import_module(module_path)
31
+ app_instance = getattr(module, attr_name)
32
+ return app_instance
33
+ except ModuleNotFoundError as e:
34
+ typer.secho(f"āŒ Module not found: {module_path}", fg=typer.colors.RED)
35
+ typer.secho(f" Error: {e}", fg=typer.colors.YELLOW)
36
+ raise typer.Exit(1)
37
+ except AttributeError:
38
+ typer.secho(
39
+ f"āŒ Attribute '{attr_name}' not found in module '{module_path}'",
40
+ fg=typer.colors.RED,
41
+ )
42
+ raise typer.Exit(1)
43
+
44
+
45
+ @app.command()
46
+ def export(
47
+ app_path: str = typer.Argument(
48
+ ..., help="App path in format 'module:attribute' (e.g., 'app:app')"
49
+ ),
50
+ output: Optional[Path] = typer.Option(
51
+ None, "--output", "-o", help="Output file path (default: stdout)"
52
+ ),
53
+ indent: int = typer.Option(2, "--indent", "-i", help="JSON indentation level"),
54
+ ):
55
+ """
56
+ šŸ“„ Export OpenAPI schema to JSON.
57
+
58
+ Example:
59
+ tachyon openapi export app:app
60
+ tachyon openapi export app:app -o openapi.json
61
+ tachyon openapi export app:app | jq .
62
+ """
63
+ # Add current directory to path for imports
64
+ import sys
65
+
66
+ sys.path.insert(0, str(Path.cwd()))
67
+
68
+ typer.echo(f"šŸ“„ Loading app from: {app_path}", err=True)
69
+
70
+ app_instance = _load_app(app_path)
71
+
72
+ # Get OpenAPI schema
73
+ try:
74
+ schema = app_instance.openapi_generator.get_openapi_schema()
75
+ except AttributeError:
76
+ typer.secho(
77
+ "āŒ The loaded object doesn't appear to be a Tachyon app",
78
+ fg=typer.colors.RED,
79
+ )
80
+ raise typer.Exit(1)
81
+
82
+ # Convert to JSON
83
+ json_output = json.dumps(schema, indent=indent, ensure_ascii=False)
84
+
85
+ if output:
86
+ output.write_text(json_output)
87
+ typer.echo(f"āœ… Schema exported to: {output}", err=True)
88
+ else:
89
+ typer.echo(json_output)
90
+
91
+
92
+ @app.command()
93
+ def validate(
94
+ schema_path: Path = typer.Argument(..., help="Path to OpenAPI schema file"),
95
+ ):
96
+ """
97
+ āœ… Validate an OpenAPI schema file.
98
+
99
+ Example:
100
+ tachyon openapi validate openapi.json
101
+ """
102
+ if not schema_path.exists():
103
+ typer.secho(f"āŒ File not found: {schema_path}", fg=typer.colors.RED)
104
+ raise typer.Exit(1)
105
+
106
+ try:
107
+ content = schema_path.read_text()
108
+ schema = json.loads(content)
109
+
110
+ # Basic validation
111
+ required_fields = ["openapi", "info", "paths"]
112
+ missing = [f for f in required_fields if f not in schema]
113
+
114
+ if missing:
115
+ typer.secho(
116
+ f"āŒ Invalid schema: missing required fields: {missing}",
117
+ fg=typer.colors.RED,
118
+ )
119
+ raise typer.Exit(1)
120
+
121
+ typer.secho("āœ… Schema is valid!", fg=typer.colors.GREEN)
122
+ typer.echo(f" OpenAPI version: {schema.get('openapi')}")
123
+ typer.echo(f" Title: {schema.get('info', {}).get('title')}")
124
+ typer.echo(f" Paths: {len(schema.get('paths', {}))}")
125
+
126
+ except json.JSONDecodeError as e:
127
+ typer.secho(f"āŒ Invalid JSON: {e}", fg=typer.colors.RED)
128
+ raise typer.Exit(1)
@@ -0,0 +1,69 @@
1
+ """
2
+ Tachyon CLI - Main entry point
3
+
4
+ Commands:
5
+ - tachyon new <project> Create new project
6
+ - tachyon generate Generate components (alias: g)
7
+ - tachyon openapi OpenAPI utilities
8
+ - tachyon lint Code quality (ruff wrapper)
9
+ """
10
+
11
+ import typer
12
+ from typing import Optional
13
+ from pathlib import Path
14
+
15
+ from .commands import generate, openapi, lint
16
+
17
+ app = typer.Typer(
18
+ name="tachyon",
19
+ help="šŸš€ Tachyon CLI - Fast API development toolkit",
20
+ add_completion=False,
21
+ no_args_is_help=True,
22
+ )
23
+
24
+ # Register sub-commands
25
+ app.add_typer(
26
+ generate.app,
27
+ name="generate",
28
+ help="Generate components (service, controller, etc.)",
29
+ )
30
+ app.add_typer(generate.app, name="g", help="Alias for 'generate'", hidden=True)
31
+ app.add_typer(openapi.app, name="openapi", help="OpenAPI schema utilities")
32
+ app.add_typer(lint.app, name="lint", help="Code quality tools (ruff wrapper)")
33
+
34
+
35
+ @app.command()
36
+ def new(
37
+ name: str = typer.Argument(..., help="Project name"),
38
+ path: Optional[Path] = typer.Option(
39
+ None,
40
+ "--path",
41
+ "-p",
42
+ help="Parent directory for the project (default: current directory)",
43
+ ),
44
+ ):
45
+ """
46
+ šŸ—ļø Create a new Tachyon project with clean architecture.
47
+
48
+ Example:
49
+ tachyon new my-api
50
+ tachyon new my-api --path ./projects
51
+ """
52
+ from .commands.new import create_project
53
+
54
+ create_project(name, path)
55
+
56
+
57
+ @app.command()
58
+ def version():
59
+ """Show Tachyon version."""
60
+ typer.echo("Tachyon API v0.6.6")
61
+
62
+
63
+ def main():
64
+ """Entry point for the CLI."""
65
+ app()
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
@@ -0,0 +1,8 @@
1
+ """
2
+ CLI Templates for code generation.
3
+ """
4
+
5
+ from .project import ProjectTemplates
6
+ from .service import ServiceTemplates
7
+
8
+ __all__ = ["ProjectTemplates", "ServiceTemplates"]
@@ -0,0 +1,194 @@
1
+ """
2
+ Project scaffolding templates.
3
+ """
4
+
5
+
6
+ class ProjectTemplates:
7
+ """Templates for `tachyon new` command."""
8
+
9
+ APP = '''"""
10
+ Main application entry point.
11
+ """
12
+
13
+ import uvicorn
14
+ from tachyon_api import Tachyon
15
+ from config import settings
16
+
17
+ # Initialize app
18
+ app = Tachyon()
19
+
20
+
21
+ @app.get("/")
22
+ def root():
23
+ """Health check endpoint."""
24
+ return {"status": "ok", "app": settings.APP_NAME}
25
+
26
+
27
+ @app.get("/health")
28
+ def health():
29
+ """Detailed health check."""
30
+ return {
31
+ "status": "healthy",
32
+ "version": settings.VERSION,
33
+ }
34
+
35
+
36
+ # Import and register routers here
37
+ # from modules.users import router as users_router
38
+ # app.include_router(users_router)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ uvicorn.run(
43
+ "app:app",
44
+ host=settings.HOST,
45
+ port=settings.PORT,
46
+ reload=settings.DEBUG,
47
+ )
48
+ '''
49
+
50
+ CONFIG = '''"""
51
+ Application configuration.
52
+ """
53
+
54
+ import os
55
+
56
+
57
+ class Settings:
58
+ """Application settings loaded from environment variables."""
59
+
60
+ APP_NAME: str = os.getenv("APP_NAME", "Tachyon API")
61
+ VERSION: str = os.getenv("VERSION", "0.1.0")
62
+ DEBUG: bool = os.getenv("DEBUG", "true").lower() == "true"
63
+
64
+ # Server
65
+ HOST: str = os.getenv("HOST", "0.0.0.0")
66
+ PORT: int = int(os.getenv("PORT", "8000"))
67
+
68
+ # Database (example)
69
+ # DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./app.db")
70
+
71
+
72
+ settings = Settings()
73
+ '''
74
+
75
+ REQUIREMENTS = """# Tachyon API Framework
76
+ tachyon-api>=0.6.0
77
+
78
+ # Server
79
+ uvicorn[standard]>=0.30.0
80
+
81
+ # Development
82
+ pytest>=8.0.0
83
+ pytest-asyncio>=0.23.0
84
+ httpx>=0.27.0
85
+ ruff>=0.4.0
86
+ """
87
+
88
+ MODULES_INIT = '''"""
89
+ Application modules.
90
+
91
+ Each module follows clean architecture:
92
+ - controller: HTTP endpoints (router)
93
+ - service: Business logic
94
+ - repository: Data access
95
+ - dto: Data transfer objects
96
+ """
97
+ '''
98
+
99
+ SHARED_INIT = '''"""
100
+ Shared utilities and dependencies.
101
+ """
102
+
103
+ from .exceptions import *
104
+ from .dependencies import *
105
+ '''
106
+
107
+ SHARED_EXCEPTIONS = '''"""
108
+ Custom application exceptions.
109
+ """
110
+
111
+ from tachyon_api import HTTPException
112
+
113
+
114
+ class NotFoundError(HTTPException):
115
+ """Resource not found."""
116
+
117
+ def __init__(self, resource: str, id: str):
118
+ super().__init__(
119
+ status_code=404,
120
+ detail=f"{resource} with id '{id}' not found"
121
+ )
122
+
123
+
124
+ class UnauthorizedError(HTTPException):
125
+ """Authentication required."""
126
+
127
+ def __init__(self, detail: str = "Not authenticated"):
128
+ super().__init__(
129
+ status_code=401,
130
+ detail=detail,
131
+ headers={"WWW-Authenticate": "Bearer"}
132
+ )
133
+
134
+
135
+ class ForbiddenError(HTTPException):
136
+ """Access denied."""
137
+
138
+ def __init__(self, detail: str = "Access denied"):
139
+ super().__init__(status_code=403, detail=detail)
140
+
141
+
142
+ class BadRequestError(HTTPException):
143
+ """Invalid request."""
144
+
145
+ def __init__(self, detail: str):
146
+ super().__init__(status_code=400, detail=detail)
147
+
148
+
149
+ class ConflictError(HTTPException):
150
+ """Resource conflict (e.g., duplicate)."""
151
+
152
+ def __init__(self, detail: str):
153
+ super().__init__(status_code=409, detail=detail)
154
+ '''
155
+
156
+ SHARED_DEPENDENCIES = '''"""
157
+ Shared dependencies for dependency injection.
158
+ """
159
+
160
+ from tachyon_api import Depends
161
+ from tachyon_api.security import OAuth2PasswordBearer
162
+
163
+ # Example: OAuth2 token dependency
164
+ # oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
165
+ #
166
+ # async def get_current_user(token: str = Depends(oauth2_scheme)):
167
+ # # Validate token and return user
168
+ # pass
169
+ '''
170
+
171
+ TESTS_CONFTEST = '''"""
172
+ Pytest configuration and fixtures.
173
+ """
174
+
175
+ import pytest
176
+ from httpx import AsyncClient, ASGITransport
177
+
178
+
179
+ @pytest.fixture
180
+ def app():
181
+ """Create test application instance."""
182
+ from app import app
183
+ return app
184
+
185
+
186
+ @pytest.fixture
187
+ async def client(app):
188
+ """Create async test client."""
189
+ async with AsyncClient(
190
+ transport=ASGITransport(app=app),
191
+ base_url="http://test"
192
+ ) as client:
193
+ yield client
194
+ '''