-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync_actions.py
More file actions
executable file
·156 lines (114 loc) · 5.05 KB
/
Copy pathsync_actions.py
File metadata and controls
executable file
·156 lines (114 loc) · 5.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/usr/bin/env python3
"""Regenerate src/waapi/_generated.py and tests/test_generated_actions.py.
The API has 122 client actions. Writing them by hand would put every future
API change in four places -- the n8n node, the MCP tools, the PHP SDKs and
here -- so they are generated from the same OpenAPI specification the others
use, by `sdk:generate-methods` in the proxy repository.
Everything generated lands in its own module, which this script overwrites
whole. Nothing hand-written lives there, so a regeneration can never lose
someone's edit; the hand-written core stays in _actions.py and composes the
generated classes in.
python3 scripts/sync_actions.py ../eazewhatsapp-proxy
"""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
HEADER = '''"""Client actions generated from the WaAPI OpenAPI specification.
DO NOT EDIT. Regenerate with:
python3 scripts/sync_actions.py ../eazewhatsapp-proxy
Hand-written methods belong in _actions.py, which composes these classes in.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
class _Calls:
"""The contract the mixins rely on, declared for the type checker only."""
if TYPE_CHECKING:
def action(
self,
name: str,
payload: dict[str, Any] | None = None,
*,
instance_id: int | str | None = None,
) -> Any: ...
'''
TEST_HEADER = '''"""Generated payload tests -- one per client action.
DO NOT EDIT. Regenerate with:
python3 scripts/sync_actions.py ../eazewhatsapp-proxy
These methods hold no logic: they name an action and forward named arguments.
So their real failure modes are a wrong action string and a parameter that is
dropped or swapped with its neighbour, and both are visible in the request
that leaves the SDK. Sample values carry the parameter's own name for exactly
that reason -- identical values could not tell a swap from a correct call.
"""
from __future__ import annotations
'''
def generate(proxy: Path, *args: str) -> str:
"""Run the generator and return only the emitted code.
The command reports how many methods it wrote on stdout as well, which is
useful in a terminal and a syntax error in a Python file.
"""
result = subprocess.run(
[sys.executable and "php", "artisan", "sdk:generate-methods", *args],
cwd=proxy,
capture_output=True,
text=True,
check=True,
)
body = re.sub(r"^\s*INFO\s+\d+ methods generated\.\s*$", "", result.stdout, flags=re.MULTILINE)
return body.rstrip() + "\n"
def count_methods(source: str) -> int:
return len(re.findall(r"^\s+(?:async )?def \w+\(", source, flags=re.MULTILINE))
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("proxy", type=Path, help="path to the eazewhatsapp-proxy checkout")
args = parser.parse_args()
proxy = args.proxy.expanduser().resolve()
if not (proxy / "artisan").is_file():
raise SystemExit(f"not a Laravel checkout: {proxy}")
sync = generate(proxy, "--flavour=python")
asyncronous = generate(proxy, "--flavour=python-async")
tests = generate(proxy, "--flavour=python", "--tests")
n_sync, n_async = count_methods(sync), count_methods(asyncronous)
if n_sync != n_async:
raise SystemExit(f"sync/async surfaces differ: {n_sync} vs {n_async}")
if n_sync == 0:
raise SystemExit("the generator emitted nothing -- check the spec path")
module = (
HEADER
+ "\n\nclass GeneratedActions(_Calls):\n"
+ ' """Every client action, blocking."""\n'
+ sync
+ "\n\nclass GeneratedAsyncActions(_Calls):\n"
+ ' """Every client action, awaited."""\n'
+ asyncronous
)
(ROOT / "src" / "waapi" / "_generated.py").write_text(module)
(ROOT / "tests" / "test_generated_actions.py").write_text(TEST_HEADER + tests)
written = [
ROOT / "src" / "waapi" / "_generated.py",
ROOT / "tests" / "test_generated_actions.py",
]
tidy(written)
print(f"wrote {n_sync} sync and {n_async} async methods, and {count_tests(tests)} tests")
return 0
def tidy(paths: list[Path]) -> None:
"""Bring the generated files up to the project's lint rules.
Emitting blank lines to PEP 8's satisfaction from a PHP string builder is
possible and pointless: the formatter already knows the rules, and letting
it run means a lint failure can never be something a human has to fix by
hand in a file marked DO NOT EDIT.
"""
for command in (["ruff", "check", "--fix", "--quiet"], ["ruff", "format", "--quiet"]):
try:
subprocess.run([*command, *map(str, paths)], cwd=ROOT, check=True)
except FileNotFoundError:
print("ruff not on PATH -- generated files left unformatted", file=sys.stderr)
return
def count_tests(source: str) -> int:
return len(re.findall(r"^def test_", source, flags=re.MULTILINE))
if __name__ == "__main__":
raise SystemExit(main())