jararaca 0.3.12a18__py3-none-any.whl → 0.3.12a20__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.

Potentially problematic release.


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

@@ -24,7 +24,7 @@ from typing import (
24
24
  from uuid import UUID
25
25
 
26
26
  from fastapi import Request, Response, UploadFile
27
- from fastapi.params import Body, Cookie, Depends, Header, Path, Query
27
+ from fastapi.params import Body, Cookie, Depends, Form, Header, Path, Query
28
28
  from fastapi.security.http import HTTPBase
29
29
  from pydantic import BaseModel, PlainValidator
30
30
  from pydantic_core import PydanticUndefined
@@ -48,6 +48,58 @@ def is_constant(name: str) -> bool:
48
48
  return CONSTANT_PATTERN.match(name) is not None
49
49
 
50
50
 
51
+ def unwrap_annotated_type(field_type: Any) -> tuple[Any, list[Any]]:
52
+ """
53
+ Recursively unwrap Annotated types to find the real underlying type.
54
+
55
+ Args:
56
+ field_type: The type to unwrap, which may be deeply nested Annotated types
57
+
58
+ Returns:
59
+ A tuple of (unwrapped_type, all_metadata) where:
60
+ - unwrapped_type is the final non-Annotated type
61
+ - all_metadata is a list of all metadata from all Annotated layers
62
+ """
63
+ all_metadata = []
64
+ current_type = field_type
65
+
66
+ while get_origin(current_type) == Annotated:
67
+ # Collect metadata from current layer
68
+ if hasattr(current_type, "__metadata__"):
69
+ all_metadata.extend(current_type.__metadata__)
70
+
71
+ # Move to the next inner type
72
+ if hasattr(current_type, "__args__") and len(current_type.__args__) > 0:
73
+ current_type = current_type.__args__[0]
74
+ else:
75
+ break
76
+
77
+ return current_type, all_metadata
78
+
79
+
80
+ def is_upload_file_type(field_type: Any) -> bool:
81
+ """
82
+ Check if a type is UploadFile or a list/array of UploadFile.
83
+
84
+ Args:
85
+ field_type: The type to check
86
+
87
+ Returns:
88
+ True if it's UploadFile or list[UploadFile], False otherwise
89
+ """
90
+ if field_type == UploadFile:
91
+ return True
92
+
93
+ # Check for list[UploadFile], List[UploadFile], etc.
94
+ origin = get_origin(field_type)
95
+ if origin in (list, frozenset, set):
96
+ args = getattr(field_type, "__args__", ())
97
+ if args and args[0] == UploadFile:
98
+ return True
99
+
100
+ return False
101
+
102
+
51
103
  def should_exclude_field(
52
104
  field_name: str, field_type: Any, basemodel_type: Type[Any]
53
105
  ) -> bool:
@@ -87,7 +139,8 @@ def should_exclude_field(
87
139
 
88
140
  # Check for Annotated types with Field metadata
89
141
  if get_origin(field_type) == Annotated:
90
- for metadata in field_type.__metadata__:
142
+ unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
143
+ for metadata in all_metadata:
91
144
  # Check if this is a Pydantic Field by looking for expected attributes
92
145
  if hasattr(metadata, "exclude") or hasattr(metadata, "alias"):
93
146
  # Check if Field has exclude=True
@@ -182,7 +235,8 @@ def has_default_value(
182
235
 
183
236
  # Check for Annotated types with Field metadata that have defaults
184
237
  if get_origin(field_type) == Annotated:
185
- for metadata in field_type.__metadata__:
238
+ unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
239
+ for metadata in all_metadata:
186
240
  # Check if this is a Pydantic Field with a default
187
241
  if hasattr(metadata, "default") and hasattr(
188
242
  metadata, "exclude"
@@ -333,16 +387,18 @@ def get_field_type_for_ts(field_type: Any, context_suffix: str = "") -> Any:
333
387
  [get_field_type_for_ts(x, context_suffix) for x in field_type.__args__]
334
388
  )
335
389
  if (get_origin(field_type) == Annotated) and (len(field_type.__args__) > 0):
390
+ unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
391
+
336
392
  if (
337
393
  plain_validator := next(
338
- (x for x in field_type.__metadata__ if isinstance(x, PlainValidator)),
394
+ (x for x in all_metadata if isinstance(x, PlainValidator)),
339
395
  None,
340
396
  )
341
397
  ) is not None:
342
398
  return get_field_type_for_ts(
343
399
  plain_validator.json_schema_input_type, context_suffix
344
400
  )
345
- return get_field_type_for_ts(field_type.__args__[0], context_suffix)
401
+ return get_field_type_for_ts(unwrapped_type, context_suffix)
346
402
  return "unknown"
347
403
 
348
404
 
@@ -625,6 +681,21 @@ def write_microservice_to_typescript_interface(
625
681
  // noinspection JSUnusedGlobalSymbols
626
682
 
627
683
  import { HttpService, HttpBackend, HttpBackendRequest, ResponseType, createClassQueryHooks , createClassMutationHooks, createClassInfiniteQueryHooks, paginationModelByFirstArgPaginationFilter } from "@jararaca/core";
684
+
685
+ function makeFormData(data: Record<string, any>): FormData {
686
+ const formData = new FormData();
687
+ for (const key in data) {
688
+ if (Array.isArray(data[key])) {
689
+ data[key].forEach((item: any) => {
690
+ formData.append(key, item);
691
+ });
692
+ } else {
693
+ formData.append(key, data[key]);
694
+ }
695
+ }
696
+ return formData;
697
+ }
698
+
628
699
  export type WebSocketMessageMap = {
629
700
  %s
630
701
  }
@@ -768,10 +839,21 @@ def write_rest_controller_to_typescript_interface(
768
839
  class_buffer.write(f'\t\t\tmethod: "{mapping.method}",\n')
769
840
 
770
841
  endpoint_path = parse_path_with_params(mapping.path, arg_params_spec)
771
- final_path = "/".join(
772
- s.strip("/") for s in [rest_controller.path, endpoint_path]
773
- )
774
- class_buffer.write(f"\t\t\tpath: `/{final_path}`,\n")
842
+
843
+ # Properly handle path joining to avoid double slashes
844
+ controller_path = rest_controller.path or ""
845
+ path_parts = []
846
+
847
+ if controller_path and controller_path.strip("/"):
848
+ path_parts.append(controller_path.strip("/"))
849
+ if endpoint_path and endpoint_path.strip("/"):
850
+ path_parts.append(endpoint_path.strip("/"))
851
+
852
+ final_path = "/".join(path_parts) if path_parts else ""
853
+ # Ensure the path starts with a single slash
854
+ formatted_path = f"/{final_path}" if final_path else "/"
855
+
856
+ class_buffer.write(f"\t\t\tpath: `{formatted_path}`,\n")
775
857
 
776
858
  # Sort path params
777
859
  path_params = sorted(
@@ -803,10 +885,25 @@ def write_rest_controller_to_typescript_interface(
803
885
  class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
804
886
  class_buffer.write("\t\t\t},\n")
805
887
 
806
- if (
807
- body := next((x for x in arg_params_spec if x.type_ == "body"), None)
808
- ) is not None:
809
- class_buffer.write(f"\t\t\tbody: {body.name}\n")
888
+ # Check if we need to use FormData (for file uploads or form parameters)
889
+ form_params = [param for param in arg_params_spec if param.type_ == "form"]
890
+ body_param = next((x for x in arg_params_spec if x.type_ == "body"), None)
891
+
892
+ if form_params:
893
+ # Use FormData for file uploads and form parameters
894
+ class_buffer.write("\t\t\tbody: makeFormData({\n")
895
+
896
+ # Add form parameters (including file uploads)
897
+ for param in form_params:
898
+ class_buffer.write(f'\t\t\t\t"{param.name}": {param.name},\n')
899
+
900
+ # Add body parameter if it exists alongside form params
901
+ if body_param:
902
+ class_buffer.write(f"\t\t\t\t...{body_param.name},\n")
903
+
904
+ class_buffer.write("\t\t\t})\n")
905
+ elif body_param is not None:
906
+ class_buffer.write(f"\t\t\tbody: {body_param.name}\n")
810
907
  else:
811
908
  class_buffer.write("\t\t\tbody: undefined\n")
812
909
 
@@ -862,7 +959,7 @@ EXCLUDED_REQUESTS_TYPES = [Request, Response]
862
959
 
863
960
  @dataclass
864
961
  class HttpParemeterSpec:
865
- type_: Literal["query", "path", "body", "header", "cookie"]
962
+ type_: Literal["query", "path", "body", "header", "cookie", "form"]
866
963
  name: str
867
964
  required: bool
868
965
  argument_type_str: str
@@ -910,15 +1007,16 @@ def extract_parameters(
910
1007
  if is_primitive(member):
911
1008
 
912
1009
  if get_origin(member) is Annotated:
1010
+ unwrapped_type, all_metadata = unwrap_annotated_type(member)
913
1011
  if (
914
1012
  plain_validator := next(
915
- (x for x in member.__metadata__ if isinstance(x, PlainValidator)),
1013
+ (x for x in all_metadata if isinstance(x, PlainValidator)),
916
1014
  None,
917
1015
  )
918
1016
  ) is not None:
919
1017
  mapped_types.add(plain_validator.json_schema_input_type)
920
1018
  return parameters_list, mapped_types
921
- return extract_parameters(member.__args__[0], controller, mapping)
1019
+ return extract_parameters(unwrapped_type, controller, mapping)
922
1020
  return parameters_list, mapped_types
923
1021
 
924
1022
  if hasattr(member, "__bases__"):
@@ -938,8 +1036,21 @@ def extract_parameters(
938
1036
  continue
939
1037
 
940
1038
  if get_origin(parameter_type) == Annotated:
941
- annotated_type_hook = parameter_type.__metadata__[0]
942
- annotated_type = parameter_type.__args__[0]
1039
+ unwrapped_type, all_metadata = unwrap_annotated_type(parameter_type)
1040
+ # Look for FastAPI parameter annotations in all metadata layers
1041
+ annotated_type_hook = None
1042
+ for metadata in all_metadata:
1043
+ if isinstance(
1044
+ metadata, (Header, Cookie, Form, Body, Query, Path, Depends)
1045
+ ):
1046
+ annotated_type_hook = metadata
1047
+ break
1048
+
1049
+ if annotated_type_hook is None and all_metadata:
1050
+ # Fallback to first metadata if no FastAPI annotation found
1051
+ annotated_type_hook = all_metadata[0]
1052
+
1053
+ annotated_type = unwrapped_type
943
1054
  if isinstance(annotated_type_hook, Header):
944
1055
  mapped_types.add(str)
945
1056
  parameters_list.append(
@@ -960,6 +1071,16 @@ def extract_parameters(
960
1071
  argument_type_str=get_field_type_for_ts(str),
961
1072
  )
962
1073
  )
1074
+ elif isinstance(annotated_type_hook, Form):
1075
+ mapped_types.add(annotated_type)
1076
+ parameters_list.append(
1077
+ HttpParemeterSpec(
1078
+ type_="form",
1079
+ name=parameter_name,
1080
+ required=True,
1081
+ argument_type_str=get_field_type_for_ts(annotated_type),
1082
+ )
1083
+ )
963
1084
  elif isinstance(annotated_type_hook, Body):
964
1085
  mapped_types.update(extract_all_envolved_types(parameter_type))
965
1086
  # For body parameters, use Input suffix if it's a split model
@@ -1067,11 +1188,11 @@ def extract_parameters(
1067
1188
  )
1068
1189
  else:
1069
1190
  mapped_types.add(annotated_type)
1070
- # Special handling for UploadFile - should go to body, not query
1071
- if annotated_type == UploadFile:
1191
+ # Special handling for UploadFile and list[UploadFile] - should be treated as form data
1192
+ if is_upload_file_type(annotated_type):
1072
1193
  parameters_list.append(
1073
1194
  HttpParemeterSpec(
1074
- type_="body",
1195
+ type_="form",
1075
1196
  name=parameter_name,
1076
1197
  required=True,
1077
1198
  argument_type_str=get_field_type_for_ts(annotated_type),
@@ -1117,12 +1238,12 @@ def extract_parameters(
1117
1238
  ),
1118
1239
  )
1119
1240
  )
1120
- elif parameter_type == UploadFile:
1121
- # UploadFile should always go to body, not query parameters
1241
+ elif parameter_type == UploadFile or is_upload_file_type(parameter_type):
1242
+ # UploadFile and list[UploadFile] should be treated as form data
1122
1243
  mapped_types.add(parameter_type)
1123
1244
  parameters_list.append(
1124
1245
  HttpParemeterSpec(
1125
- type_="body",
1246
+ type_="form",
1126
1247
  name=parameter_name,
1127
1248
  required=True,
1128
1249
  argument_type_str=get_field_type_for_ts(parameter_type),
@@ -1156,11 +1277,11 @@ def extract_parameters(
1156
1277
  )
1157
1278
  else:
1158
1279
  mapped_types.add(parameter_type)
1159
- # Special handling for UploadFile - should go to body, not query
1160
- if parameter_type == UploadFile:
1280
+ # Special handling for UploadFile and list[UploadFile] - should be treated as form data
1281
+ if is_upload_file_type(parameter_type):
1161
1282
  parameters_list.append(
1162
1283
  HttpParemeterSpec(
1163
- type_="body",
1284
+ type_="form",
1164
1285
  name=parameter_name,
1165
1286
  required=True,
1166
1287
  argument_type_str=get_field_type_for_ts(parameter_type),
@@ -1196,21 +1317,22 @@ def extract_parameters(
1196
1317
  for _, parameter_type in parameter_members.items():
1197
1318
  if is_primitive(parameter_type.annotation):
1198
1319
  if get_origin(parameter_type.annotation) is not None:
1199
- if (
1200
- get_origin(parameter_type.annotation) == Annotated
1201
- and (
1202
- plain_validator := next(
1203
- (
1204
- x
1205
- for x in parameter_type.annotation.__metadata__
1206
- if isinstance(x, PlainValidator)
1207
- ),
1208
- None,
1209
- )
1320
+ if get_origin(parameter_type.annotation) == Annotated:
1321
+ unwrapped_type, all_metadata = unwrap_annotated_type(
1322
+ parameter_type.annotation
1210
1323
  )
1211
- is not None
1212
- ):
1213
- mapped_types.add(plain_validator.json_schema_input_type)
1324
+ plain_validator = next(
1325
+ (
1326
+ x
1327
+ for x in all_metadata
1328
+ if isinstance(x, PlainValidator)
1329
+ ),
1330
+ None,
1331
+ )
1332
+ if plain_validator is not None:
1333
+ mapped_types.add(
1334
+ plain_validator.json_schema_input_type
1335
+ )
1214
1336
  else:
1215
1337
  args = parameter_type.annotation.__args__
1216
1338
  mapped_types.update(args)
@@ -1241,22 +1363,15 @@ def extract_all_envolved_types(field_type: Any) -> set[Any]:
1241
1363
 
1242
1364
  if is_primitive(field_type):
1243
1365
  if get_origin(field_type) is not None:
1244
- if (
1245
- get_origin(field_type) == Annotated
1246
- and (
1247
- plain_validator := next(
1248
- (
1249
- x
1250
- for x in field_type.__metadata__
1251
- if isinstance(x, PlainValidator)
1252
- ),
1253
- None,
1254
- )
1366
+ if get_origin(field_type) == Annotated:
1367
+ unwrapped_type, all_metadata = unwrap_annotated_type(field_type)
1368
+ plain_validator = next(
1369
+ (x for x in all_metadata if isinstance(x, PlainValidator)),
1370
+ None,
1255
1371
  )
1256
- is not None
1257
- ):
1258
- mapped_types.add(plain_validator.json_schema_input_type)
1259
- return mapped_types
1372
+ if plain_validator is not None:
1373
+ mapped_types.add(plain_validator.json_schema_input_type)
1374
+ return mapped_types
1260
1375
  else:
1261
1376
  mapped_types.update(
1262
1377
  *[extract_all_envolved_types(arg) for arg in field_type.__args__]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jararaca
3
- Version: 0.3.12a18
3
+ Version: 0.3.12a20
4
4
  Summary: A simple and fast API framework for Python
5
5
  Home-page: https://github.com/LuscasLeo/jararaca
6
6
  Author: Lucas S
@@ -1,6 +1,6 @@
1
1
  LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
2
  README.md,sha256=2qMM__t_MoLKZr4IY9tXjo-Jn6LKjuHMb1qbyXpgL08,3401
3
- pyproject.toml,sha256=aj3QfPDz-158WddZDVdzbZgEm4SRalAyvU5X0-FpkTU,2041
3
+ pyproject.toml,sha256=uJ0U1QV3oT9pdznIF6jWS3yTivDWOsFWynfAES9yqIU,2041
4
4
  jararaca/__init__.py,sha256=vK3zyIVLckwZgj1FPX6jzSbzaSWmSy3wQ2KMwmpJnmg,22046
5
5
  jararaca/__main__.py,sha256=-O3vsB5lHdqNFjUtoELDF81IYFtR-DSiiFMzRaiSsv4,67
6
6
  jararaca/broker_backend/__init__.py,sha256=GzEIuHR1xzgCJD4FE3harNjoaYzxHMHoEL0_clUaC-k,3528
@@ -70,12 +70,12 @@ jararaca/tools/app_config/decorators.py,sha256=-ckkMZ1dswOmECdo1rFrZ15UAku--txaN
70
70
  jararaca/tools/app_config/interceptor.py,sha256=HV8h4AxqUc_ACs5do4BSVlyxlRXzx7HqJtoVO9tfRnQ,2611
71
71
  jararaca/tools/typescript/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
72
  jararaca/tools/typescript/decorators.py,sha256=y1zBq8mBZ8CBXlZ0nKy2RyIgCvP9kp4elACbaC6dptQ,2946
73
- jararaca/tools/typescript/interface_parser.py,sha256=KSJgJqhZS243L5d005Ph_f-RiZP37rgeF0Ql6iN856w,49016
73
+ jararaca/tools/typescript/interface_parser.py,sha256=Nsct1hdF35yAvXrXL-MVIC-uGKeQ3CY3xn-ixXfUidQ,53545
74
74
  jararaca/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  jararaca/utils/rabbitmq_utils.py,sha256=ytdAFUyv-OBkaVnxezuJaJoLrmN7giZgtKeet_IsMBs,10918
76
76
  jararaca/utils/retry.py,sha256=DzPX_fXUvTqej6BQ8Mt2dvLo9nNlTBm7Kx2pFZ26P2Q,4668
77
- jararaca-0.3.12a18.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
78
- jararaca-0.3.12a18.dist-info/METADATA,sha256=uDHK7-0B144S_kuJ57BELyRoJDANDRe-3Tib4N4wxXY,4996
79
- jararaca-0.3.12a18.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
80
- jararaca-0.3.12a18.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
81
- jararaca-0.3.12a18.dist-info/RECORD,,
77
+ jararaca-0.3.12a20.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
78
+ jararaca-0.3.12a20.dist-info/METADATA,sha256=f2WNsZHRbUMukKcfHiuZ08BNQuziV1ZEjigRDcS9Et8,4996
79
+ jararaca-0.3.12a20.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
80
+ jararaca-0.3.12a20.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
81
+ jararaca-0.3.12a20.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "jararaca"
3
- version = "0.3.12a18"
3
+ version = "0.3.12a20"
4
4
  description = "A simple and fast API framework for Python"
5
5
  authors = ["Lucas S <me@luscasleo.dev>"]
6
6
  readme = "README.md"