fluidkit 0.1.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.
- fluidkit/__init__.py +47 -0
- fluidkit/core/__init__.py +35 -0
- fluidkit/core/constants.py +32 -0
- fluidkit/core/integrator.py +299 -0
- fluidkit/core/schema.py +628 -0
- fluidkit/core/type_conversion.py +296 -0
- fluidkit/core/utils.py +363 -0
- fluidkit/generators/__init__.py +51 -0
- fluidkit/generators/javascript/__init__.py +0 -0
- fluidkit/generators/typescript/__init__.py +24 -0
- fluidkit/generators/typescript/clients.py +674 -0
- fluidkit/generators/typescript/imports.py +300 -0
- fluidkit/generators/typescript/interfaces.py +433 -0
- fluidkit/generators/typescript/pipeline.py +306 -0
- fluidkit/generators/zod/__init__.py +0 -0
- fluidkit/introspection/__init__.py +21 -0
- fluidkit/introspection/models.py +355 -0
- fluidkit/introspection/parameters.py +202 -0
- fluidkit/introspection/routes.py +61 -0
- fluidkit/introspection/security.py +60 -0
- fluidkit-0.1.0.dist-info/METADATA +242 -0
- fluidkit-0.1.0.dist-info/RECORD +24 -0
- fluidkit-0.1.0.dist-info/WHEEL +5 -0
- fluidkit-0.1.0.dist-info/top_level.txt +1 -0
fluidkit/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FluidKit - Automatic TypeScript client code generation for FastAPI
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "2.0.0"
|
|
6
|
+
|
|
7
|
+
def _check_dependencies():
|
|
8
|
+
"""Check for required dependencies and provide helpful error messages"""
|
|
9
|
+
missing = []
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import fastapi
|
|
13
|
+
except ImportError:
|
|
14
|
+
missing.append("fastapi")
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import pydantic
|
|
18
|
+
except ImportError:
|
|
19
|
+
missing.append("pydantic")
|
|
20
|
+
|
|
21
|
+
if missing:
|
|
22
|
+
deps = " and ".join(missing)
|
|
23
|
+
raise ImportError(
|
|
24
|
+
f"FluidKit requires {deps} to be installed.\n"
|
|
25
|
+
f"Install with: pip install {' '.join(missing)}\n"
|
|
26
|
+
f"FluidKit works with your existing {deps} versions."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Check dependencies on import
|
|
30
|
+
_check_dependencies()
|
|
31
|
+
|
|
32
|
+
# Import main API only after dependency check
|
|
33
|
+
from .core.schema import LanguageType
|
|
34
|
+
from .core.integrator import integrate, introspect_only, generate_only
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
# Main functions
|
|
38
|
+
'integrate',
|
|
39
|
+
'introspect_only',
|
|
40
|
+
'generate_only',
|
|
41
|
+
|
|
42
|
+
# Enums
|
|
43
|
+
'LanguageType',
|
|
44
|
+
|
|
45
|
+
# Version
|
|
46
|
+
'__version__'
|
|
47
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core FluidKit components - for advanced users and plugin developers
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Main data structures
|
|
6
|
+
from .schema import (
|
|
7
|
+
FluidKitApp,
|
|
8
|
+
RouteNode,
|
|
9
|
+
ModelNode,
|
|
10
|
+
Field,
|
|
11
|
+
FieldAnnotation,
|
|
12
|
+
FieldConstraints,
|
|
13
|
+
ModuleLocation,
|
|
14
|
+
|
|
15
|
+
# Enums
|
|
16
|
+
BaseType,
|
|
17
|
+
LanguageType,
|
|
18
|
+
ContainerType,
|
|
19
|
+
ParameterType,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Main integration function
|
|
23
|
+
from .integrator import integrate, introspect_only, generate_only
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Functions
|
|
27
|
+
'integrate', 'introspect_only', 'generate_only',
|
|
28
|
+
|
|
29
|
+
# Data structures
|
|
30
|
+
'FluidKitApp', 'RouteNode', 'ModelNode', 'Field',
|
|
31
|
+
'FieldAnnotation', 'FieldConstraints', 'ModuleLocation',
|
|
32
|
+
|
|
33
|
+
# Enums
|
|
34
|
+
'LanguageType', 'BaseType', 'ContainerType', 'ParameterType',
|
|
35
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FluidKit constants for runtime imports and code generation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class FluidKitRuntime:
|
|
6
|
+
"""FluidKit runtime function and type names"""
|
|
7
|
+
|
|
8
|
+
# TypeScript runtime exports
|
|
9
|
+
API_RESULT_TYPE = "ApiResult"
|
|
10
|
+
GET_BASE_URL_FN = "getBaseUrl"
|
|
11
|
+
HANDLE_RESPONSE_FN = "handleResponse"
|
|
12
|
+
|
|
13
|
+
# Runtime file locations
|
|
14
|
+
RUNTIME_DIR = ".fluidkit"
|
|
15
|
+
RUNTIME_FILE = "runtime.ts"
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def get_all_imports(cls) -> list[str]:
|
|
19
|
+
"""Get all runtime imports for TypeScript"""
|
|
20
|
+
return [cls.API_RESULT_TYPE, cls.GET_BASE_URL_FN, cls.HANDLE_RESPONSE_FN]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GenerationPaths:
|
|
24
|
+
"""Standard paths for code generation"""
|
|
25
|
+
|
|
26
|
+
FLUIDKIT_DIR = ".fluidkit"
|
|
27
|
+
TYPESCRIPT_RUNTIME = "runtime.ts"
|
|
28
|
+
|
|
29
|
+
# Future language runtimes
|
|
30
|
+
PYTHON_RUNTIME = "runtime.py"
|
|
31
|
+
JAVASCRIPT_RUNTIME = "runtime.js"
|
|
32
|
+
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FluidKit V2 App Integration
|
|
3
|
+
|
|
4
|
+
Main integration API for FluidKit with FastAPI applications with multi-language support.
|
|
5
|
+
Orchestrates route collection, model discovery, and optional code generation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from fastapi import FastAPI
|
|
12
|
+
from typing import List, Dict, Tuple
|
|
13
|
+
from fastapi.routing import APIRoute
|
|
14
|
+
|
|
15
|
+
from fluidkit.introspection.routes import route_to_node
|
|
16
|
+
from fluidkit.introspection.models import discover_models_from_routes
|
|
17
|
+
from fluidkit.core.schema import FluidKitApp, RouteNode, LanguageType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def integrate(
|
|
24
|
+
app: FastAPI,
|
|
25
|
+
lang: str = "typescript",
|
|
26
|
+
strategy: str = "mirror",
|
|
27
|
+
verbose: bool = False,
|
|
28
|
+
**options
|
|
29
|
+
) -> Tuple[FluidKitApp, Dict[str, str]]:
|
|
30
|
+
"""
|
|
31
|
+
Integrate FluidKit with FastAPI app using runtime introspection.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
app: FastAPI application instance
|
|
35
|
+
lang: Target language for code generation ("ts"/"typescript" default)
|
|
36
|
+
strategy: Generation strategy ("co-locate" or "mirror")
|
|
37
|
+
verbose: Enable detailed logging
|
|
38
|
+
**options: Additional options (project_root, runtime config, etc.)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
(FluidKitApp, generated_files_dict)
|
|
42
|
+
"""
|
|
43
|
+
if verbose:
|
|
44
|
+
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
|
|
45
|
+
|
|
46
|
+
project_root = options.get('project_root') or str(Path.cwd().resolve())
|
|
47
|
+
|
|
48
|
+
if verbose:
|
|
49
|
+
logger.info("Starting FluidKit integration with FastAPI app")
|
|
50
|
+
logger.debug(f"Project root: {project_root}")
|
|
51
|
+
|
|
52
|
+
# Collect and convert routes
|
|
53
|
+
api_routes = _collect_fastapi_routes(app)
|
|
54
|
+
route_nodes = _convert_routes_to_nodes(api_routes)
|
|
55
|
+
|
|
56
|
+
# Discover models (project types only)
|
|
57
|
+
model_nodes = discover_models_from_routes(route_nodes, project_root)
|
|
58
|
+
|
|
59
|
+
# Build FluidKitApp
|
|
60
|
+
fluid_app = FluidKitApp(
|
|
61
|
+
models=model_nodes,
|
|
62
|
+
routes=route_nodes,
|
|
63
|
+
app_instance=app,
|
|
64
|
+
metadata={'project_root': project_root, **options}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if verbose:
|
|
68
|
+
logger.info(f"Introspection complete: {len(route_nodes)} routes, {len(model_nodes)} models")
|
|
69
|
+
|
|
70
|
+
# Generate TypeScript files
|
|
71
|
+
normalized_lang = _normalize_language(lang)
|
|
72
|
+
|
|
73
|
+
if normalized_lang == LanguageType.TYPESCRIPT:
|
|
74
|
+
generated_files = _generate_and_write_typescript(fluid_app, strategy, verbose, **options)
|
|
75
|
+
|
|
76
|
+
if not verbose:
|
|
77
|
+
print(f"FluidKit: Generated {len(generated_files)} TypeScript files ({strategy} strategy)")
|
|
78
|
+
|
|
79
|
+
return fluid_app, generated_files
|
|
80
|
+
|
|
81
|
+
else:
|
|
82
|
+
raise NotImplementedError(f"Language '{lang}' not yet supported. Currently supported: ts, typescript")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _generate_and_write_typescript(
|
|
86
|
+
fluid_app: FluidKitApp,
|
|
87
|
+
strategy: str,
|
|
88
|
+
verbose: bool,
|
|
89
|
+
**options
|
|
90
|
+
) -> Dict[str, str]:
|
|
91
|
+
"""Generate TypeScript files and write them to disk."""
|
|
92
|
+
from fluidkit.generators.typescript.pipeline import generate_typescript_files
|
|
93
|
+
|
|
94
|
+
generated_files = generate_typescript_files(
|
|
95
|
+
fluid_app=fluid_app,
|
|
96
|
+
strategy=strategy,
|
|
97
|
+
**options
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
_write_generated_files(generated_files, verbose)
|
|
101
|
+
return generated_files
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _write_generated_files(generated_files: Dict[str, str], verbose: bool):
|
|
105
|
+
"""Write generated files to disk with auto-generated headers."""
|
|
106
|
+
for file_path, content in generated_files.items():
|
|
107
|
+
try:
|
|
108
|
+
file_path_obj = Path(file_path)
|
|
109
|
+
file_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
header = _get_file_header(file_path_obj.suffix)
|
|
112
|
+
final_content = header + content
|
|
113
|
+
|
|
114
|
+
with open(file_path_obj, 'w', encoding='utf-8') as f:
|
|
115
|
+
f.write(final_content)
|
|
116
|
+
|
|
117
|
+
if verbose:
|
|
118
|
+
logger.debug(f"Generated: {file_path}")
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
error_msg = f"Failed to write {file_path}: {e}"
|
|
122
|
+
if verbose:
|
|
123
|
+
logger.error(error_msg)
|
|
124
|
+
else:
|
|
125
|
+
print(f"ā {error_msg}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _get_file_header(file_extension: str) -> str:
|
|
129
|
+
"""Get auto-generated file header based on file type."""
|
|
130
|
+
if file_extension == '.ts':
|
|
131
|
+
return '''/**
|
|
132
|
+
* Auto-generated by FluidKit from FastAPI routes and models - DO NOT EDIT
|
|
133
|
+
* Changes will be overwritten on regeneration.
|
|
134
|
+
*/
|
|
135
|
+
|
|
136
|
+
'''
|
|
137
|
+
elif file_extension == '.py':
|
|
138
|
+
return '''"""
|
|
139
|
+
Auto-generated by FluidKit from FastAPI routes and models - DO NOT EDIT
|
|
140
|
+
Changes will be overwritten on regeneration.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
'''
|
|
144
|
+
else:
|
|
145
|
+
return '''/**
|
|
146
|
+
* Auto-generated by FluidKit from FastAPI routes and models - DO NOT EDIT
|
|
147
|
+
* Changes will be overwritten on regeneration.
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
'''
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _normalize_language(lang: str) -> LanguageType:
|
|
154
|
+
"""Normalize language string to LanguageType enum."""
|
|
155
|
+
lang_lower = lang.lower()
|
|
156
|
+
|
|
157
|
+
if lang_lower in ["ts", "typescript"]:
|
|
158
|
+
return LanguageType.TYPESCRIPT
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
valid_langs = ["ts", "typescript"]
|
|
162
|
+
raise ValueError(f"Unsupported language '{lang}'. Supported: {', '.join(valid_langs)}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _collect_fastapi_routes(app: FastAPI) -> List[APIRoute]:
|
|
166
|
+
"""Collect user-defined API routes from FastAPI app."""
|
|
167
|
+
user_routes = []
|
|
168
|
+
|
|
169
|
+
for route in app.routes:
|
|
170
|
+
if isinstance(route, APIRoute) and _is_user_defined_route(route):
|
|
171
|
+
user_routes.append(route)
|
|
172
|
+
|
|
173
|
+
return user_routes
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _convert_routes_to_nodes(api_routes: List[APIRoute]) -> List[RouteNode]:
|
|
177
|
+
"""Convert FastAPI APIRoute objects to RouteNode objects."""
|
|
178
|
+
route_nodes = []
|
|
179
|
+
|
|
180
|
+
for route in api_routes:
|
|
181
|
+
try:
|
|
182
|
+
route_node = route_to_node(route)
|
|
183
|
+
if route_node:
|
|
184
|
+
route_nodes.append(route_node)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.warning(f"Failed to convert route {route.path}: {e}")
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
return route_nodes
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _is_user_defined_route(route: APIRoute) -> bool:
|
|
193
|
+
"""Determine if route is user-defined using module-based filtering."""
|
|
194
|
+
endpoint = route.endpoint
|
|
195
|
+
|
|
196
|
+
if (not endpoint or not callable(endpoint) or
|
|
197
|
+
not hasattr(endpoint, '__name__') or endpoint.__name__ == '<lambda>' or
|
|
198
|
+
not hasattr(endpoint, '__module__') or not route.methods):
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
endpoint_module = endpoint.__module__
|
|
202
|
+
system_prefixes = ('fastapi.', 'starlette.')
|
|
203
|
+
|
|
204
|
+
if any(endpoint_module.startswith(prefix) for prefix in system_prefixes):
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
valid_methods = {'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'}
|
|
208
|
+
if not any(method in valid_methods for method in route.methods):
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
if hasattr(endpoint, 'app') and hasattr(endpoint, '__call__'):
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def introspect_only(app: FastAPI, **options) -> FluidKitApp:
|
|
218
|
+
"""Convenience function for introspection only (no code generation)."""
|
|
219
|
+
project_root = options.get('project_root') or str(Path.cwd().resolve())
|
|
220
|
+
|
|
221
|
+
api_routes = _collect_fastapi_routes(app)
|
|
222
|
+
route_nodes = _convert_routes_to_nodes(api_routes)
|
|
223
|
+
model_nodes = discover_models_from_routes(route_nodes, project_root)
|
|
224
|
+
|
|
225
|
+
fluid_app = FluidKitApp(
|
|
226
|
+
models=model_nodes,
|
|
227
|
+
routes=route_nodes,
|
|
228
|
+
app_instance=app,
|
|
229
|
+
metadata={'project_root': project_root, **options}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
print(f"FluidKit: Introspected {len(route_nodes)} routes, {len(model_nodes)} models")
|
|
233
|
+
return fluid_app
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def generate_only(app: FastAPI, strategy: str = "mirror", **options) -> Dict[str, str]:
|
|
237
|
+
"""Convenience function to generate files without writing to disk."""
|
|
238
|
+
from fluidkit.generators.typescript.pipeline import generate_typescript_files
|
|
239
|
+
|
|
240
|
+
project_root = options.get('project_root') or str(Path.cwd().resolve())
|
|
241
|
+
|
|
242
|
+
api_routes = _collect_fastapi_routes(app)
|
|
243
|
+
route_nodes = _convert_routes_to_nodes(api_routes)
|
|
244
|
+
model_nodes = discover_models_from_routes(route_nodes, project_root)
|
|
245
|
+
|
|
246
|
+
fluid_app = FluidKitApp(
|
|
247
|
+
models=model_nodes,
|
|
248
|
+
routes=route_nodes,
|
|
249
|
+
app_instance=app,
|
|
250
|
+
metadata={'project_root': project_root, **options}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
generated_files = generate_typescript_files(
|
|
254
|
+
fluid_app=fluid_app,
|
|
255
|
+
strategy=strategy,
|
|
256
|
+
**options
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
print(f"FluidKit: Generated {len(generated_files)} TypeScript files ({strategy} strategy) - not written to disk")
|
|
260
|
+
return generated_files
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# === TESTING FUNCTION === #
|
|
264
|
+
|
|
265
|
+
def test_integration():
|
|
266
|
+
"""Test the integration with file writing."""
|
|
267
|
+
try:
|
|
268
|
+
from tests.sample.app import app
|
|
269
|
+
|
|
270
|
+
print("=== FLUIDKIT V2 INTEGRATION TEST ===")
|
|
271
|
+
|
|
272
|
+
# Test 1: Default integration (generates and writes files)
|
|
273
|
+
print("\n1. Default integration (mirror strategy):")
|
|
274
|
+
fluid_app, files = integrate(app)
|
|
275
|
+
|
|
276
|
+
# Test 2: Co-locate strategy
|
|
277
|
+
print("\n2. Co-locate strategy:")
|
|
278
|
+
fluid_app, files = integrate(app, strategy="co-locate")
|
|
279
|
+
|
|
280
|
+
# Test 3: Introspection only
|
|
281
|
+
print("\n3. Introspection only:")
|
|
282
|
+
fluid_app = introspect_only(app)
|
|
283
|
+
|
|
284
|
+
# Test 4: Generate only (don't write)
|
|
285
|
+
print("\n4. Generate only (no file writing):")
|
|
286
|
+
files = generate_only(app)
|
|
287
|
+
|
|
288
|
+
print("\nā
All tests passed!")
|
|
289
|
+
|
|
290
|
+
except ImportError:
|
|
291
|
+
print("ā Could not import v2.examples.test - ensure example files exist")
|
|
292
|
+
except Exception as e:
|
|
293
|
+
print(f"ā Test failed: {e}")
|
|
294
|
+
import traceback
|
|
295
|
+
traceback.print_exc()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
test_integration()
|