cadwyn 3.1.0__tar.gz → 3.1.2__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.

Potentially problematic release.


This version of cadwyn might be problematic. Click here for more details.

Files changed (34) hide show
  1. {cadwyn-3.1.0 → cadwyn-3.1.2}/PKG-INFO +6 -1
  2. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/_compat.py +0 -2
  3. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/_package_utils.py +1 -0
  4. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_asts.py +4 -2
  5. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_common.py +1 -1
  6. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_main.py +4 -9
  7. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_plugins/class_migrations.py +2 -4
  8. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_plugins/class_rebuilding.py +0 -2
  9. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_plugins/latest_version_aliasing.py +5 -6
  10. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/routing.py +27 -23
  11. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/structure/data.py +2 -2
  12. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/structure/schemas.py +1 -1
  13. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/structure/versions.py +23 -21
  14. {cadwyn-3.1.0 → cadwyn-3.1.2}/pyproject.toml +43 -15
  15. {cadwyn-3.1.0 → cadwyn-3.1.2}/setup.py +1 -1
  16. {cadwyn-3.1.0 → cadwyn-3.1.2}/LICENSE +0 -0
  17. {cadwyn-3.1.0 → cadwyn-3.1.2}/README.md +0 -0
  18. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/__init__.py +0 -0
  19. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/__main__.py +0 -0
  20. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/_utils.py +0 -0
  21. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/README.md +0 -0
  22. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/__init__.py +0 -0
  23. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_plugins/__init__.py +0 -0
  24. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_plugins/class_renaming.py +0 -0
  25. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_plugins/import_auto_adding.py +0 -0
  26. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/codegen/_plugins/module_migrations.py +0 -0
  27. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/exceptions.py +0 -0
  28. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/main.py +0 -0
  29. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/py.typed +0 -0
  30. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/structure/__init__.py +0 -0
  31. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/structure/common.py +0 -0
  32. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/structure/endpoints.py +0 -0
  33. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/structure/enums.py +0 -0
  34. {cadwyn-3.1.0 → cadwyn-3.1.2}/cadwyn/structure/modules.py +0 -0
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cadwyn
3
- Version: 3.1.0
3
+ Version: 3.1.2
4
4
  Summary: Production-ready community-driven modern Stripe-like API versioning in FastAPI
5
5
  Home-page: https://github.com/zmievsa/cadwyn
6
6
  License: MIT
7
+ Keywords: python,api,json-schema,stripe,versioning,code-generation,hints,api-versioning,pydantic,fastapi,python310,python311,python312
7
8
  Author: Stanislav Zmiev
8
9
  Author-email: zmievsa@gmail.com
9
10
  Requires-Python: >=3.10,<4.0
@@ -22,6 +23,9 @@ Classifier: Programming Language :: Python :: 3
22
23
  Classifier: Programming Language :: Python :: 3.10
23
24
  Classifier: Programming Language :: Python :: 3.11
24
25
  Classifier: Programming Language :: Python :: 3
26
+ Classifier: Programming Language :: Python :: 3.10
27
+ Classifier: Programming Language :: Python :: 3.11
28
+ Classifier: Programming Language :: Python :: 3.12
25
29
  Classifier: Topic :: Internet
26
30
  Classifier: Topic :: Internet :: WWW/HTTP
27
31
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
@@ -37,6 +41,7 @@ Requires-Dist: pydantic (>=1.0.0)
37
41
  Requires-Dist: typer (>=0.7.0); extra == "cli"
38
42
  Requires-Dist: typing-extensions
39
43
  Requires-Dist: verselect (>=0.0.6)
44
+ Project-URL: Documentation, https://docs.cadwyn.dev
40
45
  Project-URL: Repository, https://github.com/zmievsa/cadwyn
41
46
  Description-Content-Type: text/markdown
42
47
 
@@ -63,7 +63,6 @@ def get_attrs_that_are_not_from_field_and_that_are_from_field(value: type):
63
63
 
64
64
  @dataclasses.dataclass(slots=True)
65
65
  class PydanticFieldWrapper:
66
- # TODO: What's the difference between these two? I keep forgetting
67
66
  annotation: Any
68
67
 
69
68
  init_model_field: dataclasses.InitVar[ModelField] # pyright: ignore[reportGeneralTypeIssues]
@@ -92,7 +91,6 @@ class PydanticFieldWrapper:
92
91
  else:
93
92
  setattr(self.field_info, name, value)
94
93
 
95
- # TODO: If we ever have performance issues with this -- it makes sense to cache and update it in update_attribute
96
94
  @property
97
95
  def passed_field_attributes(self):
98
96
  if PYDANTIC_V2:
@@ -29,6 +29,7 @@ def get_version_dir_path(template_package: ModuleType, version: date) -> Path:
29
29
 
30
30
 
31
31
  def get_package_path_from_module(template_package: ModuleType) -> Path:
32
+ # Can be cached in the future to gain some speedups
32
33
  file = inspect.getsourcefile(template_package)
33
34
 
34
35
  # I am too lazy to reproduce this error correctly
@@ -126,9 +126,10 @@ def transform_type(value: type) -> Any:
126
126
  + ")"
127
127
  )
128
128
  else:
129
- # TODO: Finish this comment :D
130
129
  # In pydantic V1:
131
- # MRO of conint looks like: []
130
+ # MRO of constr looks like: [ConstrainedStrValue, pydantic.types.ConstrainedStr, str, object]
131
+ # -2 -1
132
+ # ^^^
132
133
  value = value.mro()[-2]
133
134
 
134
135
  return value.__name__
@@ -181,6 +182,7 @@ def _get_lambda_source_from_default_factory(source: str) -> str:
181
182
 
182
183
 
183
184
  def read_python_module(module: ModuleType) -> str:
185
+ # Can be cached in the future to gain some speedups
184
186
  try:
185
187
  return inspect.getsource(module)
186
188
  except OSError as e:
@@ -113,7 +113,7 @@ class GlobalCodegenContext:
113
113
  @dataclasses.dataclass(slots=True, kw_only=True)
114
114
  class CodegenContext(GlobalCodegenContext):
115
115
  # This attribute is extremely useful for calculating relative imports
116
- index_of_latest_schema_dir_in_module_python_path: int
116
+ index_of_latest_package_dir_in_module_python_path: int
117
117
  module_python_path: str
118
118
  module_path: Path
119
119
  template_module: ModuleType
@@ -92,7 +92,6 @@ def _generate_versioned_directories(
92
92
  codegen_plugins: Collection[CodegenPlugin],
93
93
  migration_plugins: Collection[MigrationPlugin],
94
94
  ):
95
- # TODO: An alternative structure for module python path: An object similar to pathlib.Path with .name, etc
96
95
  for version in versions:
97
96
  global_context = GlobalCodegenContext(
98
97
  current_version=version,
@@ -113,7 +112,6 @@ def _generate_directory_for_version(
113
112
  version: Version,
114
113
  global_context: GlobalCodegenContext,
115
114
  ):
116
- # TODO: This call can be optimized
117
115
  template_dir = get_package_path_from_module(template_package)
118
116
  version_dir = get_version_dir_path(template_package, version.value)
119
117
  for (
@@ -178,27 +176,24 @@ def _build_context(
178
176
  parsed_file,
179
177
  module_python_path,
180
178
  )
181
- version_dir = template_dir.with_name(version_dir.name)
182
- index_of_latest_schema_dir_in_module_python_path = get_index_of_latest_schema_dir_in_module_python_path(
179
+ index_of_latest_package_dir_in_module_python_path = get_index_of_latest_schema_dir_in_module_python_path(
183
180
  template_module,
184
- version_dir,
181
+ template_dir.with_name(version_dir.name),
185
182
  )
186
- context: CodegenContext = CodegenContext(
183
+ return CodegenContext(
187
184
  current_version=global_context.current_version,
188
185
  versions=global_context.versions,
189
186
  schemas=global_context.schemas,
190
187
  enums=global_context.enums,
191
188
  modules=global_context.modules,
192
189
  extra=global_context.extra,
193
- index_of_latest_schema_dir_in_module_python_path=index_of_latest_schema_dir_in_module_python_path,
190
+ index_of_latest_package_dir_in_module_python_path=index_of_latest_package_dir_in_module_python_path,
194
191
  module_python_path=module_python_path,
195
192
  all_names_defined_on_toplevel_of_file=all_names_defined_at_toplevel_of_file,
196
193
  template_module=template_module,
197
194
  module_path=parallel_file,
198
195
  )
199
196
 
200
- return context
201
-
202
197
 
203
198
  def _generate_parallel_directory(
204
199
  template_module: ModuleType,
@@ -38,8 +38,6 @@ def _apply_alter_schema_instructions(
38
38
  alter_schema_instructions: Sequence[AlterSchemaSubInstruction | AlterSchemaInstruction],
39
39
  version_change_name: str,
40
40
  ):
41
- # TODO: If we have a request migration for an endpoint instead of a schema and we haven't found that endpoint
42
- # during codegen -- raise an error or maybe add an argument that controlls that. Or maybe this is overengineering..
43
41
  for alter_schema_instruction in alter_schema_instructions:
44
42
  schema = alter_schema_instruction.schema
45
43
  schema_path = get_cls_pythonpath(schema)
@@ -118,10 +116,10 @@ def _add_field_to_model(
118
116
  )
119
117
 
120
118
  model.fields[alter_schema_instruction.field_name] = PydanticFieldWrapper(
121
- annotation_ast=None, # TODO: Get this from migration
119
+ annotation_ast=None,
122
120
  annotation=alter_schema_instruction.type,
123
121
  init_model_field=alter_schema_instruction.field,
124
- field_ast=None, # TODO: Get this from migration
122
+ field_ast=None,
125
123
  )
126
124
 
127
125
 
@@ -114,13 +114,11 @@ def _render_annotation(annotation: Any):
114
114
 
115
115
 
116
116
  def _generate_field_ast(field: PydanticFieldWrapper):
117
- # TODO: Make sure that cadwyn doesn't remove OLD property definitions
118
117
  if field.field_ast is not None:
119
118
  # We do this because next plugins **might** use a transformer which will edit the ast within the field
120
119
  # and break rendering
121
120
  return copy.deepcopy(field.field_ast)
122
121
  passed_attrs = field.passed_field_attributes
123
- # TODO: This is None check feels buggy
124
122
  if is_pydantic_constrained_type(field.annotation) and field.annotation_ast is None:
125
123
  (
126
124
  attrs_that_are_only_in_contype,
@@ -21,7 +21,7 @@ class LatestVersionAliasingPlugin:
21
21
  imports = _prepare_imports_from_version_dirs(
22
22
  context.template_module,
23
23
  ["latest"],
24
- context.index_of_latest_schema_dir_in_module_python_path,
24
+ context.index_of_latest_package_dir_in_module_python_path,
25
25
  )
26
26
 
27
27
  import_text = ast.unparse(imports[0].get_ast()) + " # noqa: F403"
@@ -64,7 +64,6 @@ class _ImportedModule:
64
64
  module = f"{self.path}.{self.name}"
65
65
  name = ast.alias(name="*")
66
66
  level = self.how_far_up_is_base_schema_dir_from_current_module
67
- # TODO: Add a testcase where is_package == True and level == 3
68
67
  if self.is_package and level == 2:
69
68
  level -= 1
70
69
  return ast.ImportFrom(
@@ -77,7 +76,7 @@ class _ImportedModule:
77
76
  def _prepare_imports_from_version_dirs(
78
77
  original_module: ModuleType,
79
78
  version_dir_names: Collection[str],
80
- index_of_latest_schema_dir_in_pythonpath: int,
79
+ index_of_latest_package_dir_in_pythonpath: int,
81
80
  ) -> list[_ImportedModule]:
82
81
  # package.latest -> from .. import latest
83
82
  # package.latest.module -> from ...latest import module
@@ -90,16 +89,16 @@ def _prepare_imports_from_version_dirs(
90
89
  # package.latest.subpackage.module -> from ...latest.subpackage.module import *
91
90
 
92
91
  original_module_parts = original_module.__name__.split(".")
93
- original_module_parts[index_of_latest_schema_dir_in_pythonpath] = "{}"
92
+ original_module_parts[index_of_latest_package_dir_in_pythonpath] = "{}"
94
93
  how_far_up_is_base_schema_dir_from_current_module = (
95
- len(original_module_parts) - index_of_latest_schema_dir_in_pythonpath
94
+ len(original_module_parts) - index_of_latest_package_dir_in_pythonpath
96
95
  )
97
96
  is_package = original_module_parts[-1] == "__init__"
98
97
  if is_package:
99
98
  original_module_parts.pop(-1)
100
99
 
101
100
  package_name = original_module_parts[-1]
102
- package_path = original_module_parts[index_of_latest_schema_dir_in_pythonpath:-1]
101
+ package_path = original_module_parts[index_of_latest_package_dir_in_pythonpath:-1]
103
102
  import_pythonpath_template = ".".join(package_path)
104
103
  absolute_python_path_template = ".".join(original_module_parts)
105
104
  return [
@@ -41,7 +41,6 @@ from starlette.routing import (
41
41
  request_response,
42
42
  )
43
43
  from typing_extensions import Self, assert_never
44
- from verselect.routing import VERSION_HEADER_FORMAT
45
44
 
46
45
  from cadwyn._compat import model_fields, rebuild_fastapi_body_param
47
46
  from cadwyn._package_utils import get_package_path_from_module, get_version_dir_path
@@ -56,12 +55,6 @@ from cadwyn.structure.endpoints import (
56
55
  )
57
56
  from cadwyn.structure.versions import _CADWYN_REQUEST_PARAM_NAME, _CADWYN_RESPONSE_PARAM_NAME, VersionChange
58
57
 
59
- __all__ = [
60
- "generate_versioned_routers",
61
- "VersionedAPIRouter",
62
- "VERSION_HEADER_FORMAT",
63
- ]
64
-
65
58
  _T = TypeVar("_T", bound=Callable[..., Any])
66
59
  _R = TypeVar("_R", bound=fastapi.routing.APIRouter)
67
60
  # This is a hack we do because we can't guarantee how the user will use the router.
@@ -127,8 +120,10 @@ class _EndpointTransformer(Generic[_R]):
127
120
  ]
128
121
 
129
122
  def transform(self) -> dict[VersionDate, _R]:
123
+ schema_to_internal_request_body_representation = _extract_internal_request_schemas_from_router(
124
+ self.parent_router
125
+ )
130
126
  router = deepcopy(self.parent_router)
131
- schema_to_internal_request_body_representation = _extract_internal_request_schemas_from_router(router)
132
127
  router_infos: dict[VersionDate, _RouterInfo] = {}
133
128
  routes_with_migrated_requests = {}
134
129
  route_bodies_with_migrated_requests: set[type[BaseModel]] = set()
@@ -157,8 +152,6 @@ class _EndpointTransformer(Generic[_R]):
157
152
  f"{self.routes_that_never_existed}",
158
153
  )
159
154
 
160
- # BEWARE: We assume that the order of routes didn't change.
161
- # TODO: Make a test suite checking that it doesn't change
162
155
  for route_index, latest_route in enumerate(self.parent_router.routes):
163
156
  if not isinstance(latest_route, APIRoute):
164
157
  continue
@@ -166,11 +159,10 @@ class _EndpointTransformer(Generic[_R]):
166
159
  copy_of_dependant = deepcopy(latest_route.dependant)
167
160
  # Remember this: if len(body_params) == 1, then route.body_schema == route.dependant.body_params[0]
168
161
  if len(copy_of_dependant.body_params) == 1:
169
- body_param: FastAPIModelField = copy_of_dependant.body_params[0]
170
- body_schema = body_param.type_
171
- new_type = schema_to_internal_request_body_representation.get(body_schema, body_schema)
172
- new_body_param = rebuild_fastapi_body_param(body_param, new_type)
173
- copy_of_dependant.body_params = [new_body_param]
162
+ self._replace_internal_representation_with_the_versioned_schema(
163
+ copy_of_dependant,
164
+ schema_to_internal_request_body_representation,
165
+ )
174
166
 
175
167
  for older_router_info in list(router_infos.values()):
176
168
  older_route = older_router_info.router.routes[route_index]
@@ -188,11 +180,11 @@ class _EndpointTransformer(Generic[_R]):
188
180
  template_older_body_model = None
189
181
  _add_data_migrations_to_route(
190
182
  older_route,
183
+ # NOTE: The fact that we use latest here assumes that the route can never change its response schema
184
+ latest_route,
191
185
  template_older_body_model,
192
186
  older_route.body_field.alias if older_route.body_field is not None else None,
193
187
  copy_of_dependant,
194
- # NOTE: The fact that we use latest here assumes that the route can never change its response schema
195
- latest_route.response_model,
196
188
  self.versions,
197
189
  )
198
190
  for _, router_info in router_infos.items():
@@ -203,7 +195,18 @@ class _EndpointTransformer(Generic[_R]):
203
195
  ]
204
196
  return {version: router_info.router for version, router_info in router_infos.items()}
205
197
 
206
- # TODO: Simplify https://github.com/zmievsa/cadwyn/issues/28
198
+ def _replace_internal_representation_with_the_versioned_schema(
199
+ self,
200
+ copy_of_dependant: Dependant,
201
+ schema_to_internal_request_body_representation: dict[type[BaseModel], type[BaseModel]],
202
+ ):
203
+ body_param: FastAPIModelField = copy_of_dependant.body_params[0]
204
+ body_schema = body_param.type_
205
+ new_type = schema_to_internal_request_body_representation.get(body_schema, body_schema)
206
+ new_body_param = rebuild_fastapi_body_param(body_param, new_type)
207
+ copy_of_dependant.body_params = [new_body_param]
208
+
209
+ # TODO (https://github.com/zmievsa/cadwyn/issues/28): Simplify
207
210
  def _apply_endpoint_changes_to_router( # noqa: C901
208
211
  self,
209
212
  router: fastapi.routing.APIRouter,
@@ -223,7 +226,6 @@ class _EndpointTransformer(Generic[_R]):
223
226
  methods_we_should_have_applied_changes_to = instruction.endpoint_methods.copy()
224
227
 
225
228
  if isinstance(instruction, EndpointDidntExistInstruction):
226
- # TODO OPTIMIZATION:
227
229
  deleted_routes = _get_routes(
228
230
  routes,
229
231
  instruction.endpoint_path,
@@ -250,7 +252,6 @@ class _EndpointTransformer(Generic[_R]):
250
252
  ' "{version_change_name}" doesn\'t exist in a newer version'
251
253
  )
252
254
  elif isinstance(instruction, EndpointExistedInstruction):
253
- # TODO Optimization
254
255
  if original_routes:
255
256
  method_union = set()
256
257
  for original_route in original_routes:
@@ -330,7 +331,9 @@ class _EndpointTransformer(Generic[_R]):
330
331
  )
331
332
 
332
333
 
333
- def _extract_internal_request_schemas_from_router(router: fastapi.routing.APIRouter):
334
+ def _extract_internal_request_schemas_from_router(
335
+ router: fastapi.routing.APIRouter,
336
+ ) -> dict[type[BaseModel], type[BaseModel]]:
334
337
  """Please note that this functon replaces internal bodies with original bodies in the router"""
335
338
  schema_to_internal_request_body_representation = {}
336
339
 
@@ -354,6 +357,7 @@ def _extract_internal_request_schemas_from_router(router: fastapi.routing.APIRou
354
357
  route.endpoint,
355
358
  modify_annotations=_extract_internal_request_schemas_from_annotations,
356
359
  )
360
+ _remake_endpoint_dependencies(route)
357
361
  return schema_to_internal_request_body_representation
358
362
 
359
363
 
@@ -580,10 +584,10 @@ def _add_request_and_response_params(route: APIRoute):
580
584
 
581
585
  def _add_data_migrations_to_route(
582
586
  route: APIRoute,
587
+ latest_route: Any,
583
588
  template_body_field: type[BaseModel] | None,
584
589
  template_body_field_name: str | None,
585
590
  dependant_for_request_migrations: Dependant,
586
- latest_response_model: Any,
587
591
  versions: VersionBundle,
588
592
  ):
589
593
  if not (route.dependant.request_param_name and route.dependant.response_param_name): # pragma: no cover
@@ -596,8 +600,8 @@ def _add_data_migrations_to_route(
596
600
  template_body_field,
597
601
  template_body_field_name,
598
602
  route,
603
+ latest_route,
599
604
  dependant_for_request_migrations,
600
- latest_response_model,
601
605
  request_param_name=route.dependant.request_param_name,
602
606
  response_param_name=route.dependant.response_param_name,
603
607
  )(route.endpoint)
@@ -12,7 +12,7 @@ from cadwyn._utils import same_definition_as_in
12
12
  _P = ParamSpec("_P")
13
13
 
14
14
 
15
- # TODO: Add form handling https://github.com/zmievsa/cadwyn/issues/49
15
+ # TODO (https://github.com/zmievsa/cadwyn/issues/49): Add form handling
16
16
  class RequestInfo:
17
17
  __slots__ = ("body", "headers", "_cookies", "_query_params", "_request")
18
18
 
@@ -33,7 +33,7 @@ class RequestInfo:
33
33
  return self._query_params
34
34
 
35
35
 
36
- # TODO: handle media_type and background
36
+ # TODO (https://github.com/zmievsa/cadwyn/issues/111): handle _response.media_type and _response.background
37
37
  class ResponseInfo:
38
38
  __slots__ = ("body", "_response")
39
39
 
@@ -67,7 +67,7 @@ class OldSchemaFieldExistedWith:
67
67
  field: FieldInfo
68
68
 
69
69
 
70
- # TODO: Add an ability to add extras (use only pydanticV2 syntax everywhere)
70
+ # TODO (https://github.com/zmievsa/cadwyn/issues/112): Add an ability to add extras
71
71
  @dataclass(slots=True)
72
72
  class AlterFieldInstructionFactory:
73
73
  schema: type[BaseModel]
@@ -305,6 +305,7 @@ class VersionBundle:
305
305
  response: FastapiResponse,
306
306
  request_info: RequestInfo,
307
307
  current_version: VersionDate,
308
+ latest_route: APIRoute,
308
309
  ):
309
310
  path = request.scope["path"]
310
311
  method = request.method
@@ -329,8 +330,7 @@ class VersionBundle:
329
330
  response=response,
330
331
  dependant=dependant,
331
332
  body=request_info.body,
332
- # TODO: Take it from route
333
- dependency_overrides_provider=None,
333
+ dependency_overrides_provider=latest_route.dependency_overrides_provider,
334
334
  )
335
335
  if errors:
336
336
  raise RequestValidationError(_normalize_errors(errors), body=request_info.body)
@@ -340,7 +340,7 @@ class VersionBundle:
340
340
  self,
341
341
  response_info: ResponseInfo,
342
342
  current_version: VersionDate,
343
- latest_response_model: Any | None,
343
+ latest_route: APIRoute,
344
344
  path: str,
345
345
  method: str,
346
346
  ) -> ResponseInfo:
@@ -361,24 +361,24 @@ class VersionBundle:
361
361
  break
362
362
  for version_change in v.version_changes:
363
363
  if (
364
- latest_response_model
365
- and latest_response_model in version_change.alter_response_by_schema_instructions
364
+ latest_route.response_model
365
+ and latest_route.response_model in version_change.alter_response_by_schema_instructions
366
366
  ):
367
- version_change.alter_response_by_schema_instructions[latest_response_model](response_info)
367
+ version_change.alter_response_by_schema_instructions[latest_route.response_model](response_info)
368
368
  if path in version_change.alter_response_by_path_instructions:
369
369
  for instruction in version_change.alter_response_by_path_instructions[path]:
370
370
  if method in instruction.methods:
371
371
  instruction(response_info)
372
372
  return response_info
373
373
 
374
- # TODO: This function is all over the place. Refactor it and all functions it calls.
374
+ # TODO (https://github.com/zmievsa/cadwyn/issues/113): Refactor this function and all functions it calls.
375
375
  def _versioned(
376
376
  self,
377
377
  template_module_body_field_for_request_migrations: type[BaseModel] | None,
378
378
  module_body_field_name: str | None,
379
379
  route: APIRoute,
380
+ latest_route: APIRoute,
380
381
  dependant_for_request_migrations: Dependant,
381
- latest_response_model: Any,
382
382
  *,
383
383
  request_param_name: str,
384
384
  response_param_name: str,
@@ -399,11 +399,12 @@ class VersionBundle:
399
399
  kwargs,
400
400
  response,
401
401
  route,
402
+ latest_route,
402
403
  )
403
404
 
404
405
  return await self._convert_endpoint_response_to_version(
405
406
  endpoint,
406
- latest_response_model,
407
+ latest_route,
407
408
  path,
408
409
  method,
409
410
  response_param_name,
@@ -424,7 +425,7 @@ class VersionBundle:
424
425
  async def _convert_endpoint_response_to_version(
425
426
  self,
426
427
  func_to_get_response_from: Endpoint,
427
- latest_response_model: Any,
428
+ latest_route: APIRoute,
428
429
  path: str,
429
430
  method: str,
430
431
  response_param_name: str,
@@ -434,8 +435,6 @@ class VersionBundle:
434
435
  ) -> Any:
435
436
  if response_param_name == _CADWYN_RESPONSE_PARAM_NAME:
436
437
  kwargs.pop(response_param_name)
437
- # TODO: Verify that we handle fastapi.Response here
438
- # TODO: Verify that we handle fastapi.Response descendants
439
438
  if endpoint_is_async_callable:
440
439
  response_or_response_body: FastapiResponse | object = await func_to_get_response_from(**kwargs)
441
440
  else:
@@ -449,22 +448,24 @@ class VersionBundle:
449
448
  if isinstance(response_or_response_body, FastapiResponse):
450
449
  response_info = ResponseInfo(
451
450
  response_or_response_body,
452
- # TODO: Give user the ability to specify their own renderer
453
- # TODO: Only do this if there are migrations
451
+ # TODO (https://github.com/zmievsa/cadwyn/issues/51): Only do this if there are migrations
454
452
  json.loads(response_or_response_body.body) if response_or_response_body.body else None,
455
453
  )
456
454
  else:
457
- # TODO: We probably need to call this in the same way as in fastapi instead of hardcoding exclude_unset.
458
- # We have such an ability if we force passing the route into this wrapper. Or maybe not... Important!
459
455
  response_info = ResponseInfo(
460
456
  fastapi_response_dependency,
461
- _prepare_response_content(response_or_response_body, exclude_unset=False),
457
+ _prepare_response_content(
458
+ response_or_response_body,
459
+ exclude_unset=latest_route.response_model_exclude_unset,
460
+ exclude_defaults=latest_route.response_model_exclude_defaults,
461
+ exclude_none=latest_route.response_model_exclude_none,
462
+ ),
462
463
  )
463
464
 
464
465
  response_info = self._migrate_response(
465
466
  response_info,
466
467
  api_version,
467
- latest_response_model,
468
+ latest_route,
468
469
  path,
469
470
  method,
470
471
  )
@@ -476,8 +477,7 @@ class VersionBundle:
476
477
  # `RuntimeError: Response content longer than Content-Length` or
477
478
  # `Too much data for declared Content-Length`, based on the protocol
478
479
  if response_info.body is not None:
479
- # TODO: Give user the ability to specify their own renderer
480
- # TODO: Only do this if there are migrations
480
+ # TODO (https://github.com/zmievsa/cadwyn/issues/51): Only do this if there are migrations
481
481
  response_info._response.body = json.dumps(response_info.body).encode()
482
482
  return response_info._response
483
483
  return response_info.body
@@ -491,6 +491,7 @@ class VersionBundle:
491
491
  kwargs: dict[str, Any],
492
492
  response: FastapiResponse,
493
493
  route: APIRoute,
494
+ latest_route: APIRoute,
494
495
  ):
495
496
  request: FastapiRequest = kwargs[request_param_name]
496
497
  if request_param_name == _CADWYN_REQUEST_PARAM_NAME:
@@ -500,7 +501,7 @@ class VersionBundle:
500
501
  if api_version is None:
501
502
  return kwargs
502
503
 
503
- # TODO: What if the user never edits it? We just add a round of (de)serialization
504
+ # TODO (https://github.com/zmievsa/cadwyn/issues/51): Make this zero-cost for migration-less cases
504
505
  if (
505
506
  len(route.dependant.body_params) == 1
506
507
  and template_module_body_field_for_request_migrations is not None
@@ -524,6 +525,7 @@ class VersionBundle:
524
525
  response,
525
526
  request_info,
526
527
  api_version,
528
+ latest_route,
527
529
  )
528
530
  # Because we re-added it into our kwargs when we did solve_dependencies
529
531
  if request_param_name == _CADWYN_REQUEST_PARAM_NAME:
@@ -1,17 +1,36 @@
1
1
  [tool.poetry]
2
2
  name = "cadwyn"
3
- version = "3.1.0"
3
+ version = "3.1.2"
4
4
  description = "Production-ready community-driven modern Stripe-like API versioning in FastAPI"
5
5
  authors = ["Stanislav Zmiev <zmievsa@gmail.com>"]
6
6
  license = "MIT"
7
7
  readme = "README.md"
8
8
  repository = "https://github.com/zmievsa/cadwyn"
9
+ documentation = "https://docs.cadwyn.dev"
10
+ keywords = [
11
+ "python",
12
+ "api",
13
+ "json-schema",
14
+ "stripe",
15
+ "versioning",
16
+ "code-generation",
17
+ "hints",
18
+ "api-versioning",
19
+ "pydantic",
20
+ "fastapi",
21
+ "python310",
22
+ "python311",
23
+ "python312",
24
+ ]
9
25
  classifiers = [
10
26
  "Intended Audience :: Information Technology",
11
27
  "Intended Audience :: System Administrators",
12
28
  "Operating System :: OS Independent",
13
- "Programming Language :: Python :: 3",
14
29
  "Programming Language :: Python",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
15
34
  "Topic :: Internet",
16
35
  "Topic :: Software Development :: Libraries :: Application Frameworks",
17
36
  "Topic :: Software Development :: Libraries :: Python Modules",
@@ -43,7 +62,6 @@ ast-comments = "~1.2.0"
43
62
  cli = ["typer"]
44
63
 
45
64
  [tool.poetry.group.dev.dependencies]
46
- black = "*"
47
65
  ruff = "*"
48
66
  pytest = ">=7.2.1"
49
67
  pytest-cov = ">=4.0.0"
@@ -59,6 +77,7 @@ dirty-equals = "^0.6.0"
59
77
  mkdocs = "^1.5.2"
60
78
  mkdocs-material = "^9.3.1"
61
79
  python-multipart = "^0.0.6"
80
+ mkdocs-simple-hooks = "^0.1.5"
62
81
 
63
82
  [tool.poetry.scripts]
64
83
  cadwyn = "cadwyn.__main__:app"
@@ -89,17 +108,7 @@ exclude_lines = [
89
108
  "__rich_repr__",
90
109
  "__repr__",
91
110
  ]
92
- omit = [
93
- "tests/test_tutorial/test_users_example003/*",
94
- "tests/_data/*",
95
- "tests/_temp/*",
96
- "tests/test_tutorial/*/schemas/*",
97
- ]
98
-
99
- [tool.black]
100
- color = true
101
- line-length = 120
102
- target-version = ["py310"]
111
+ omit = ["./docs/plugin.py", "./site/plugin.py", "./tests/_data/_temp/**/*", "tests/tutorial/data/**/*"]
103
112
 
104
113
  [tool.pyright]
105
114
  reportMissingImports = true
@@ -120,7 +129,6 @@ reportAssertAlwaysTrue = true
120
129
  reportUnsupportedDunderAll = true
121
130
  reportUnnecessaryTypeIgnoreComment = true
122
131
  reportMissingSuperCall = true
123
- ignore=["cadwyn/_codegen/ast_comments.py"]
124
132
 
125
133
 
126
134
  [tool.ruff]
@@ -184,8 +192,28 @@ ignore = [
184
192
  "RET505", # Unnecessary `else` after `return` statement
185
193
  "N805", # First argument of a method should be named `self`
186
194
  "UP007", # Use `X | Y` for type annotations (we need this for testing and our runtime logic)
195
+
196
+ # The following rules are recommended to be ignored by ruff when using ruff format
197
+ "ISC001", # Checks for implicitly concatenated strings on a single line
198
+ "ISC002", # Checks for implicitly concatenated strings that span multiple lines
199
+ "W191", # Checks for indentation that uses tabs
200
+ "E111", # Checks for indentation with a non-multiple of 4 spaces
201
+ "E114", # Checks for indentation of comments with a non-multiple of 4 spaces
202
+ "E117", # Checks for over-indented code
203
+ "D206", # Checks for docstrings that are indented with tabs
204
+ "D300", # Checks for docstrings that use '''single quotes''' instead of """double quotes"""
205
+ "Q000", # Checks for inline strings that use single quotes or double quotes
206
+ "Q001", # Checks for multiline strings that use single quotes or double quotes
207
+ "Q002", # Checks for docstrings that use single quotes or double quotes
208
+ "Q003", # Checks for strings that include escaped quotes
209
+ "COM812", # Checks for the absence of trailing commas
210
+ "COM819", # Checks for the presence of prohibited trailing commas
187
211
  ]
188
212
 
213
+ [tool.ruff.format]
214
+ quote-style = "double"
215
+ indent-style = "space"
216
+
189
217
  [tool.ruff.per-file-ignores]
190
218
  "tests/*" = [
191
219
  "S", # ignore bandit security issues in tests
@@ -22,7 +22,7 @@ entry_points = \
22
22
 
23
23
  setup_kwargs = {
24
24
  'name': 'cadwyn',
25
- 'version': '3.1.0',
25
+ 'version': '3.1.2',
26
26
  'description': 'Production-ready community-driven modern Stripe-like API versioning in FastAPI',
27
27
  'long_description': '# Cadwyn\n\nProduction-ready community-driven modern [Stripe-like](https://stripe.com/blog/api-versioning) API versioning in FastAPI\n\n---\n\n<p align="center">\n<a href="https://github.com/zmievsa/cadwyn/actions?query=workflow%3ATests+event%3Apush+branch%3Amain" target="_blank">\n <img src="https://github.com/zmievsa/cadwyn/actions/workflows/test.yaml/badge.svg?branch=main&event=push" alt="Test">\n</a>\n<a href="https://codecov.io/gh/ovsyanka83/cadwyn" target="_blank">\n <img src="https://img.shields.io/codecov/c/github/ovsyanka83/cadwyn?color=%2334D058" alt="Coverage">\n</a>\n<a href="https://pypi.org/project/cadwyn/" target="_blank">\n <img alt="PyPI" src="https://img.shields.io/pypi/v/cadwyn?color=%2334D058&label=pypi%20package" alt="Package version">\n</a>\n<a href="https://pypi.org/project/cadwyn/" target="_blank">\n <img src="https://img.shields.io/pypi/pyversions/cadwyn?color=%2334D058" alt="Supported Python versions">\n</a>\n</p>\n\n## Who is this for?\n\nCadwyn allows you to support a single version of your code while auto-generating the schemas and routes for older versions. You keep API versioning encapsulated in small and independent "version change" modules while your business logic stays simple and knows nothing about versioning.\n\nIts [approach](https://docs.cadwyn.dev/theory/#ii-migration-based-response-building) will be useful if you want to:\n\n1. Support many API versions for a long time\n2. Effortlessly backport features and bugfixes to older API versions\n\nWhether you are a newbie in API versioning, a pro looking for a sophisticated tool, an experimenter looking to build a similar framework, or even someone who just wants to learn about all approaches to API versioning -- Cadwyn has the functionality, theory, and documentation to cover all the mentioned use cases.\n\n## Get started\n\nThe [documentation](https://docs.cadwyn.dev) has everything you need to succeed.\n',
28
28
  'author': 'Stanislav Zmiev',
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes