otterapi 0.0.4__tar.gz → 0.0.5__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.
@@ -818,3 +818,6 @@ fabric.properties
818
818
  # Android studio 3.1+ serialized cache file
819
819
  .idea/caches/build_file_checksums.ser
820
820
 
821
+
822
+ otter.yaml
823
+ otter.yml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: otterapi
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: A cute little companion that generates type-safe clients from OpenAPI documents.
5
5
  Project-URL: Source, https://github.com/danplischke/otter
6
6
  Author: Dan Plischke
@@ -81,4 +81,4 @@ def version() -> None:
81
81
 
82
82
 
83
83
  if __name__ == '__main__':
84
- generate('/Users/PLISCD/Downloads/otterapi.yml')
84
+ generate()
@@ -1,6 +1,9 @@
1
1
  import ast
2
+ import keyword
2
3
  from collections.abc import Iterable
3
4
 
5
+ PYTHON_KEYWORDS = set(keyword.kwlist)
6
+
4
7
 
5
8
  def _name(name: str) -> ast.Name:
6
9
  return ast.Name(id=name, ctx=ast.Load())
@@ -244,14 +244,16 @@ def base_async_request_fn():
244
244
 
245
245
  def get_parameters(
246
246
  parameters: list[Parameter],
247
- ) -> tuple[list[ast.arg], list[ast.arg], list[ast.expr]]:
247
+ ) -> tuple[list[ast.arg], list[ast.arg], list[ast.expr], dict[str, set[str]]]:
248
248
  args = []
249
249
  kwonlyargs = []
250
250
  kw_defaults = []
251
251
  imports = {}
252
252
 
253
253
  for param in parameters:
254
- param_name = param.name
254
+ if param.name == 'user-id':
255
+ print()
256
+ param_name = param.name_sanitized
255
257
  param_type = param.type.annotation_ast if param.type else None
256
258
  param_required = param.required
257
259
 
@@ -294,7 +296,7 @@ def build_header_params(headers: list[Parameter]) -> ast.Dict | None:
294
296
 
295
297
  return ast.Dict(
296
298
  keys=[ast.Constant(value=header.name) for header in headers],
297
- values=[_name(header.name) for header in headers],
299
+ values=[_name(header.name_sanitized) for header in headers],
298
300
  )
299
301
 
300
302
 
@@ -304,7 +306,7 @@ def build_query_params(queries: list[Parameter]) -> ast.Dict | None:
304
306
 
305
307
  return ast.Dict(
306
308
  keys=[ast.Constant(value=query.name) for query in queries],
307
- values=[_name(query.name) for query in queries],
309
+ values=[_name(query.name_sanitized) for query in queries],
308
310
  )
309
311
 
310
312
 
@@ -332,7 +334,7 @@ def build_path_params(
332
334
  # Add the formatted value for the parameter
333
335
  values.append(
334
336
  ast.FormattedValue(
335
- value=_name(path_param.name),
337
+ value=_name(path_param.name_sanitized),
336
338
  conversion=-1, # No conversion (default)
337
339
  )
338
340
  )
@@ -353,7 +355,7 @@ def build_body_params(body: Parameter | None) -> ast.expr | None:
353
355
 
354
356
  if body.type.type == 'model' or body.type.type == 'root_model':
355
357
  return _call(
356
- func=_attr(_name(body.name), 'model_dump'),
358
+ func=_attr(_name(body.name_sanitized), 'model_dump'),
357
359
  args=[],
358
360
  )
359
361
 
@@ -16,7 +16,11 @@ from otterapi.codegen.ast_utils import _all, _assign, _call, _name, _union_expr
16
16
  from otterapi.codegen.endpoints import async_request_fn, request_fn
17
17
  from otterapi.codegen.openapi_processor import OpenAPIProcessor
18
18
  from otterapi.codegen.type_generator import Endpoint, Parameter, Type, TypeGen
19
- from otterapi.codegen.utils import is_url, sanitize_identifier
19
+ from otterapi.codegen.utils import (
20
+ is_url,
21
+ sanitize_identifier,
22
+ sanitize_parameter_field_name,
23
+ )
20
24
  from otterapi.config import DocumentConfig
21
25
 
22
26
  HTTP_METHODS = [method.value.lower() for method in http.HTTPMethod]
@@ -89,6 +93,7 @@ class Codegen(OpenAPIProcessor):
89
93
  params.append(
90
94
  Parameter(
91
95
  name=param.name,
96
+ name_sanitized=sanitize_parameter_field_name(param.name),
92
97
  location=param.param_in, # query, path, header, cookie
93
98
  required=param.required or False,
94
99
  type=param_type,
@@ -114,6 +119,7 @@ class Codegen(OpenAPIProcessor):
114
119
  params.append(
115
120
  Parameter(
116
121
  name='body',
122
+ name_sanitized='body',
117
123
  location='body',
118
124
  required=body.required or False,
119
125
  type=body_type,
@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field, RootModel
10
10
 
11
11
  from otterapi.codegen.ast_utils import _call, _name, _subscript, _union_expr
12
12
  from otterapi.codegen.openapi_processor import OpenAPIProcessor
13
- from otterapi.codegen.utils import sanitize_identifier
13
+ from otterapi.codegen.utils import sanitize_identifier, sanitize_parameter_field_name
14
14
 
15
15
  _PRIMITIVE_TYPE_MAP = {
16
16
  ('string', None): str,
@@ -126,6 +126,7 @@ class Type:
126
126
  @dataclasses.dataclass
127
127
  class Parameter:
128
128
  name: str
129
+ name_sanitized: str
129
130
  location: str # query, path, header, cookie, body
130
131
  required: bool
131
132
  type: Type | None = None
@@ -261,18 +262,38 @@ class TypeGen(OpenAPIProcessor):
261
262
  if hasattr(field_schema, 'ref'):
262
263
  field_schema, _ = self._resolve_reference(field_schema)
263
264
 
265
+ field_keywords = list()
266
+
267
+ sanitized_field_name = sanitize_parameter_field_name(field_name)
268
+
264
269
  value = None
265
270
  if field_schema.default is not None and isinstance(
266
271
  field_schema.default, (str, int, float, bool)
267
272
  ):
268
- value = _call(
269
- _name(Field.__name__),
270
- keywords=[
271
- ast.keyword(arg='default', value=ast.Constant(field_schema.default))
272
- ],
273
+ field_keywords.append(
274
+ ast.keyword(arg='default', value=ast.Constant(field_schema.default))
273
275
  )
274
276
  elif field_schema.default is None and not field_schema.required:
275
- value = ast.Constant(None)
277
+ field_keywords.append(ast.keyword(arg='default', value=ast.Constant(None)))
278
+
279
+ if sanitized_field_name != field_name:
280
+ field_keywords.append(
281
+ ast.keyword(
282
+ arg='alias',
283
+ value=ast.Constant(field_name), # original name before adding _
284
+ )
285
+ )
286
+ field_name = sanitized_field_name
287
+
288
+ if field_keywords:
289
+ value = _call(
290
+ func=_name(Field.__name__),
291
+ keywords=field_keywords,
292
+ )
293
+
294
+ field_type.add_implementation_import(
295
+ module=Field.__module__, name=Field.__name__
296
+ )
276
297
 
277
298
  return ast.AnnAssign(
278
299
  target=_name(field_name),
@@ -498,7 +519,7 @@ class TypeGen(OpenAPIProcessor):
498
519
  type_ = self._create_array_type(
499
520
  schema=schema, name=schema_name, base_name=base_name
500
521
  )
501
- elif schema.type == DataType.OBJECT or schema.type == None:
522
+ elif schema.type == DataType.OBJECT or schema.type is None:
502
523
  type_ = self._create_object_type(
503
524
  schema, name=schema_name, base_name=base_name
504
525
  )
@@ -24,6 +24,33 @@ def remove_accents(input_str):
24
24
  return ''.join(c for c in nfkd_form if not unicodedata.combining(c))
25
25
 
26
26
 
27
+ def sanitize_name_python_keywords(name: str) -> str:
28
+ import keyword
29
+
30
+ if name in keyword.kwlist:
31
+ return f'{name}_'
32
+ return name
33
+
34
+
35
+ def sanitize_parameter_field_name(name: str) -> str:
36
+ """Sanitize parameter or field names to be valid Python identifiers.
37
+
38
+ - Replace spaces and hyphens with underscores
39
+ - Remove other invalid characters
40
+ - Ensure it doesn't start with a digit
41
+ """
42
+ if not name:
43
+ raise ValueError('Name cannot be empty')
44
+
45
+ sanitized = sanitize_name_python_keywords(name)
46
+ sanitized = re.sub(r'[-\s]+', '_', remove_accents(sanitized))
47
+ sanitized = re.sub(r'[^A-Za-z0-9_]', '', sanitized)
48
+
49
+ if sanitized and sanitized[0].isdigit():
50
+ sanitized = '_' + sanitized
51
+ return sanitized
52
+
53
+
27
54
  def sanitize_identifier(name: str) -> str:
28
55
  """Convert a string into a valid Python identifier.
29
56
 
@@ -76,17 +76,17 @@ exclude = [".venv", "venv", "build", "dist", "__pycache__", ".eggs"]
76
76
  extend-select = [
77
77
  "Q",
78
78
  "RUF100",
79
- "RUF018", # https://docs.astral.sh/ruff/rules/assignment-in-assert/
80
- "C90",
79
+ "RUF018", # https://docs.astral.sh/ruff/rules/assignment-in-assert,
81
80
  "UP",
82
81
  "I",
83
- "D",
84
82
  "TID251",
85
83
  ]
86
84
  flake8-quotes = { inline-quotes = "single", multiline-quotes = "double" }
87
85
  mccabe = { max-complexity = 15 }
88
86
  ignore = [
89
87
  "D100", # ignore missing docstring in module
88
+ "D101", # ignore missing in public class TODO: fix later
89
+ "D103", # ignore missing in public function TODO: fix later
90
90
  "D102", # ignore missing docstring in public method
91
91
  "D104", # ignore missing docstring in public package
92
92
  "D105", # ignore missing docstring in magic methods
File without changes
File without changes
File without changes
File without changes