wbapi-codegen 0.1.0__tar.gz → 0.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.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: wbapi-codegen
3
+ Version: 0.1.2
4
+ Summary: Code generator for wbapi-async — generates types, methods and API client from Wildberries OpenAPI specs
5
+ Project-URL: Repository, https://github.com/serdukow/wbapi-codegen
6
+ Author-email: Andrei Serdiukov <asyncdf@gmail.com>
7
+ Maintainer-email: Andrei Serdiukov <asyncdf@gmail.com>
8
+ License: MIT
9
+ Keywords: api,codegen,openapi,wb,wildberries
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Code Generators
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: pyyaml>=6.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # wbapi-codegen
20
+
21
+ Code generator for [wbapi-async](https://github.com/serdukow/wbapi-async).
@@ -0,0 +1,3 @@
1
+ # wbapi-codegen
2
+
3
+ Code generator for [wbapi-async](https://github.com/serdukow/wbapi-async).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wbapi-codegen"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Code generator for wbapi-async — generates types, methods and API client from Wildberries OpenAPI specs"
9
9
  authors = [
10
10
  { name = "Andrei Serdiukov", email = "asyncdf@gmail.com" },
@@ -12,8 +12,7 @@ Parses OpenAPI YAML files and generates:
12
12
  from __future__ import annotations
13
13
 
14
14
  import re
15
- import sys
16
- from dataclasses import dataclass, field
15
+ from dataclasses import dataclass
17
16
  from keyword import iskeyword
18
17
  from pathlib import Path
19
18
  from typing import Any
@@ -177,6 +176,7 @@ class MethodDef:
177
176
  description: str = ""
178
177
  source_url: str = ""
179
178
  type_def: TypeDef | None = None
179
+ empty_response: bool = False # True for 204 No Content or empty schema
180
180
 
181
181
 
182
182
  # ---------------------------------------------------------------------------
@@ -254,6 +254,27 @@ def _find_response_array(schema: dict, spec: dict) -> tuple[list[FieldDef], str
254
254
  return fields, dot_path
255
255
 
256
256
 
257
+ _DOC_SLUG_MAP: dict[str, str] = {
258
+ "01-general": "api-information",
259
+ "02-products": "work-with-products",
260
+ "03-orders-fbs": "orders-fbs",
261
+ "04-orders-dbw": "orders-dbw",
262
+ "05-orders-dbs": "orders-dbs",
263
+ "06-in-store-pickup": "in-store-pickup",
264
+ "07-orders-fbw": "orders-fbw",
265
+ "08-promotion": "promotion",
266
+ "09-communications": "communications",
267
+ "10-tariffs": "tariffs",
268
+ "11-analytics": "analytics",
269
+ "12-reports": "reports",
270
+ "13-finances": "financial-reports-and-accounting",
271
+ }
272
+
273
+
274
+ def _yaml_stem_to_doc_slug(stem: str) -> str:
275
+ return _DOC_SLUG_MAP.get(stem, stem)
276
+
277
+
257
278
  def parse_yaml(yaml_path: Path) -> list[MethodDef]:
258
279
  with yaml_path.open() as f:
259
280
  spec = yaml.safe_load(f)
@@ -338,14 +359,16 @@ def parse_yaml(yaml_path: Path) -> list[MethodDef]:
338
359
 
339
360
  # response type
340
361
  responses = operation.get("responses", {})
362
+ # prefer 200, then 201, then 204
341
363
  ok_response = _resolve_schema(
342
- responses.get("200", responses.get("201", {})), spec
364
+ responses.get("200", responses.get("201", responses.get("204", {}))), spec
343
365
  )
344
366
  ok_content = ok_response.get("content", {})
345
367
  ok_schema = _resolve_schema(
346
368
  ok_content.get("application/json", {}).get("schema", {}), spec
347
369
  )
348
370
  item_fields, data_key = _find_response_array(ok_schema, spec)
371
+ empty_response = not ok_content or not ok_schema
349
372
 
350
373
  # build names from summary
351
374
  if summary:
@@ -373,10 +396,15 @@ def parse_yaml(yaml_path: Path) -> list[MethodDef]:
373
396
 
374
397
  tag = (operation.get("tags") or [""])[0]
375
398
  tag_slug = re.sub(r"[^a-zA-Z0-9]", "-", tag).strip("-")
376
- path_encoded = path_str.replace("/", "~1")
399
+ path_encoded = (
400
+ path_str.replace("/", "~1")
401
+ .replace("{", "%7B")
402
+ .replace("}", "%7D")
403
+ )
377
404
  source_url = (
378
- f"https://dev.wildberries.ru/en/openapi/"
379
- f"{yaml_path.stem}#tag/{tag_slug}/paths/{path_encoded}/{http_verb}"
405
+ f"https://dev.wildberries.ru/en/docs/openapi/"
406
+ f"{_yaml_stem_to_doc_slug(yaml_path.stem)}"
407
+ f"#tag/{tag_slug}/paths/{path_encoded}/{http_verb}"
380
408
  )
381
409
 
382
410
  results.append(MethodDef(
@@ -393,6 +421,7 @@ def parse_yaml(yaml_path: Path) -> list[MethodDef]:
393
421
  params=param_fields,
394
422
  description=summary,
395
423
  source_url=source_url,
424
+ empty_response=empty_response,
396
425
  type_def=TypeDef(
397
426
  class_name=type_name,
398
427
  fields=item_fields or [],
@@ -472,6 +501,8 @@ def generate_method_file(md: MethodDef) -> str:
472
501
  lines += [' """'] + [f" {dl}" if dl else "" for dl in doc_lines] + [' """', ""]
473
502
 
474
503
  lines.append(f" __return__ = {md.return_type}")
504
+ if md.empty_response:
505
+ lines.append(" __empty_response__ = True")
475
506
  lines.append(f' __api__ = "{md.api_host}"')
476
507
  if md.path_template:
477
508
  lines.append(' __method__ = ""')
@@ -562,7 +593,7 @@ def _api_method_wrapper(md: MethodDef) -> str:
562
593
  sig_parts.append(f"{p.py_name}: {p.py_type} = {default}")
563
594
 
564
595
  sig = ", ".join(sig_parts)
565
- ret = f"list[{md.return_type}]"
596
+ ret = "None" if md.empty_response else f"list[{md.return_type}]"
566
597
 
567
598
  call_args = ", ".join(
568
599
  f"{p.py_name}={p.py_name}" for p in path_params + non_path_params
@@ -719,24 +750,28 @@ def generate_test_file(md: MethodDef) -> str:
719
750
  f"{p.py_name}={_mock_value(p.py_type, p.py_name)}" for p in required_params
720
751
  )
721
752
 
722
- mock_response = _build_mock_response(
723
- md.type_def.fields if md.type_def else [], md.data_key
724
- )
725
-
726
- # pick a field to assert on (first non-None field)
727
- assert_lines: list[str] = []
728
- if md.type_def and md.type_def.fields:
729
- for f in md.type_def.fields[:3]:
730
- val = _mock_value(f.py_type, f.alias)
731
- if val != "None":
732
- assert_lines.append(
733
- f" assert result[0].{f.py_name} == {val}"
734
- )
753
+ if md.empty_response:
754
+ mock_response = "None"
755
+ assert_lines = [" assert result is None"]
756
+ else:
757
+ mock_response = _build_mock_response(
758
+ md.type_def.fields if md.type_def else [], md.data_key
759
+ )
760
+ assert_lines = [
761
+ f" assert isinstance(result, list)",
762
+ f" assert len(result) == 1",
763
+ f" assert isinstance(result[0], {md.return_type})",
764
+ ]
765
+ if md.type_def and md.type_def.fields:
766
+ for f in md.type_def.fields[:3]:
767
+ val = _mock_value(f.py_type, f.alias)
768
+ if val != "None":
769
+ assert_lines.append(f" assert result[0].{f.py_name} == {val}")
735
770
 
736
771
  lines = [
737
772
  "import pytest",
738
773
  "",
739
- f"from wbapi_async.types.{md.return_type_file} import {md.return_type}",
774
+ *([f"from wbapi_async.types.{md.return_type_file} import {md.return_type}"] if not md.empty_response else []),
740
775
  "from tests.mocked_api import MockedAPI",
741
776
  "",
742
777
  "",
@@ -750,9 +785,6 @@ def generate_test_file(md: MethodDef) -> str:
750
785
  "",
751
786
  f" result = await api.{md.method_name}({call_args})",
752
787
  "",
753
- f" assert isinstance(result, list)",
754
- f" assert len(result) == 1",
755
- f" assert isinstance(result[0], {md.return_type})",
756
788
  *assert_lines,
757
789
  "",
758
790
  ]
@@ -855,6 +887,7 @@ def run(yaml_files: list[Path], target: Path) -> None:
855
887
  ))
856
888
  print(f"[init] {(methods_dir / '__init__.py').relative_to(target)}")
857
889
 
890
+ all_method_defs.sort(key=lambda m: m.method_name)
858
891
  api_path.write_text(generate_api_file(all_method_defs, unofficial_block))
859
892
  print(f"[api] {api_path.relative_to(target)}")
860
893
 
@@ -1,59 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: wbapi-codegen
3
- Version: 0.1.0
4
- Summary: Code generator for wbapi-async — generates types, methods and API client from Wildberries OpenAPI specs
5
- Project-URL: Repository, https://github.com/serdukow/wbapi-codegen
6
- Author-email: Andrei Serdiukov <asyncdf@gmail.com>
7
- Maintainer-email: Andrei Serdiukov <asyncdf@gmail.com>
8
- License: MIT
9
- Keywords: api,codegen,openapi,wb,wildberries
10
- Classifier: Development Status :: 3 - Alpha
11
- Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Topic :: Software Development :: Code Generators
15
- Requires-Python: >=3.10
16
- Requires-Dist: pyyaml>=6.0
17
- Description-Content-Type: text/markdown
18
-
19
- # wbapi-codegen
20
-
21
- Code generator for [wbapi-async](https://github.com/serdukow/wbapi-async).
22
-
23
- Parses Wildberries OpenAPI YAML specs and generates:
24
- - `src/wbapi_async/types/` — Pydantic response models
25
- - `src/wbapi_async/methods/` — `WbMethod` request classes
26
- - `src/wbapi_async/client/api.py` — `WbAPI` wrapper methods
27
-
28
- Files marked `__unofficial__ = True` are never overwritten.
29
-
30
- ## Install
31
-
32
- ```bash
33
- pip install wbapi-codegen
34
- ```
35
-
36
- ## Usage
37
-
38
- ```bash
39
- # process all YAMLs from .wb/swagger/ into current directory
40
- wbapi-codegen --target /path/to/wbapi-async
41
-
42
- # custom swagger directory
43
- wbapi-codegen --target /path/to/wbapi-async --swagger-dir /path/to/yaml/files
44
-
45
- # single file
46
- wbapi-codegen --target /path/to/wbapi-async /path/to/02-products.yaml
47
- ```
48
-
49
- ## How it works
50
-
51
- 1. Downloads OpenAPI YAMLs from Wildberries developer portal
52
- 2. Parses paths, parameters, request bodies and response schemas
53
- 3. Generates typed Python files following the `wbapi-async` conventions
54
- 4. Creates a PR in the `wbapi-async` repo via GitHub Actions (runs daily at 04:00 UTC)
55
-
56
- ## Setup for GitHub Actions
57
-
58
- In the `wbapi-codegen` repo, add a secret `WBAPI_PAT` — a GitHub Personal Access Token
59
- with `repo` scope on the `wbapi-async` repository.
@@ -1,41 +0,0 @@
1
- # wbapi-codegen
2
-
3
- Code generator for [wbapi-async](https://github.com/serdukow/wbapi-async).
4
-
5
- Parses Wildberries OpenAPI YAML specs and generates:
6
- - `src/wbapi_async/types/` — Pydantic response models
7
- - `src/wbapi_async/methods/` — `WbMethod` request classes
8
- - `src/wbapi_async/client/api.py` — `WbAPI` wrapper methods
9
-
10
- Files marked `__unofficial__ = True` are never overwritten.
11
-
12
- ## Install
13
-
14
- ```bash
15
- pip install wbapi-codegen
16
- ```
17
-
18
- ## Usage
19
-
20
- ```bash
21
- # process all YAMLs from .wb/swagger/ into current directory
22
- wbapi-codegen --target /path/to/wbapi-async
23
-
24
- # custom swagger directory
25
- wbapi-codegen --target /path/to/wbapi-async --swagger-dir /path/to/yaml/files
26
-
27
- # single file
28
- wbapi-codegen --target /path/to/wbapi-async /path/to/02-products.yaml
29
- ```
30
-
31
- ## How it works
32
-
33
- 1. Downloads OpenAPI YAMLs from Wildberries developer portal
34
- 2. Parses paths, parameters, request bodies and response schemas
35
- 3. Generates typed Python files following the `wbapi-async` conventions
36
- 4. Creates a PR in the `wbapi-async` repo via GitHub Actions (runs daily at 04:00 UTC)
37
-
38
- ## Setup for GitHub Actions
39
-
40
- In the `wbapi-codegen` repo, add a secret `WBAPI_PAT` — a GitHub Personal Access Token
41
- with `repo` scope on the `wbapi-async` repository.
File without changes