polyapi-python 0.3.7.dev7__py3-none-any.whl → 0.3.8__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.
- polyapi/cli.py +42 -1
- polyapi/client.py +4 -4
- polyapi/config.py +69 -1
- polyapi/deployables.py +10 -7
- polyapi/function_cli.py +4 -2
- polyapi/generate.py +141 -44
- polyapi/parser.py +7 -1
- polyapi/poly_schemas.py +159 -39
- polyapi/prepare.py +13 -2
- polyapi/schema.py +16 -2
- polyapi/typedefs.py +1 -0
- polyapi/variables.py +89 -15
- polyapi/webhook.py +22 -16
- {polyapi_python-0.3.7.dev7.dist-info → polyapi_python-0.3.8.dist-info}/METADATA +3 -2
- polyapi_python-0.3.8.dist-info/RECORD +31 -0
- polyapi_python-0.3.7.dev7.dist-info/RECORD +0 -31
- {polyapi_python-0.3.7.dev7.dist-info → polyapi_python-0.3.8.dist-info}/WHEEL +0 -0
- {polyapi_python-0.3.7.dev7.dist-info → polyapi_python-0.3.8.dist-info}/licenses/LICENSE +0 -0
- {polyapi_python-0.3.7.dev7.dist-info → polyapi_python-0.3.8.dist-info}/top_level.txt +0 -0
polyapi/cli.py
CHANGED
|
@@ -14,6 +14,16 @@ from .sync import sync_deployables
|
|
|
14
14
|
CLI_COMMANDS = ["setup", "generate", "function", "clear", "help", "update_rendered_spec"]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _get_version_string():
|
|
18
|
+
"""Get the version string for the package."""
|
|
19
|
+
try:
|
|
20
|
+
import importlib.metadata
|
|
21
|
+
version = importlib.metadata.version('polyapi-python')
|
|
22
|
+
return version
|
|
23
|
+
except Exception:
|
|
24
|
+
return "Unknown"
|
|
25
|
+
|
|
26
|
+
|
|
17
27
|
def execute_from_cli():
|
|
18
28
|
# First we setup all our argument parsing logic
|
|
19
29
|
# Then we parse the arguments (waaay at the bottom)
|
|
@@ -22,6 +32,11 @@ def execute_from_cli():
|
|
|
22
32
|
description="Manage your Poly API configurations and functions",
|
|
23
33
|
formatter_class=argparse.RawTextHelpFormatter
|
|
24
34
|
)
|
|
35
|
+
|
|
36
|
+
# Add global --version/-v flag
|
|
37
|
+
parser.add_argument('-v', '--version', action='version',
|
|
38
|
+
version=_get_version_string(),
|
|
39
|
+
help="Show version information")
|
|
25
40
|
|
|
26
41
|
subparsers = parser.add_subparsers(help="Available commands")
|
|
27
42
|
|
|
@@ -36,6 +51,9 @@ def execute_from_cli():
|
|
|
36
51
|
set_api_key_and_url(args.url, args.api_key)
|
|
37
52
|
else:
|
|
38
53
|
initialize_config(force=True)
|
|
54
|
+
# setup command should have default cache values
|
|
55
|
+
from .config import cache_generate_args
|
|
56
|
+
cache_generate_args(contexts=None, names=None, function_ids=None, no_types=False)
|
|
39
57
|
generate()
|
|
40
58
|
|
|
41
59
|
setup_parser.set_defaults(command=setup)
|
|
@@ -46,11 +64,34 @@ def execute_from_cli():
|
|
|
46
64
|
generate_parser = subparsers.add_parser("generate", help="Generates Poly library")
|
|
47
65
|
generate_parser.add_argument("--no-types", action="store_true", help="Generate SDK without type definitions")
|
|
48
66
|
generate_parser.add_argument("--contexts", type=str, required=False, help="Contexts to generate")
|
|
67
|
+
generate_parser.add_argument("--names", type=str, required=False, help="Resource names to generate (comma-separated)")
|
|
68
|
+
generate_parser.add_argument("--function-ids", type=str, required=False, help="Function IDs to generate (comma-separated)")
|
|
49
69
|
|
|
50
70
|
def generate_command(args):
|
|
71
|
+
from .config import cache_generate_args
|
|
72
|
+
|
|
51
73
|
initialize_config()
|
|
74
|
+
|
|
52
75
|
contexts = args.contexts.split(",") if args.contexts else None
|
|
53
|
-
|
|
76
|
+
names = args.names.split(",") if args.names else None
|
|
77
|
+
function_ids = args.function_ids.split(",") if args.function_ids else None
|
|
78
|
+
no_types = args.no_types
|
|
79
|
+
|
|
80
|
+
# overwrite all cached values with the values passed in from the command line
|
|
81
|
+
final_contexts = contexts
|
|
82
|
+
final_names = names
|
|
83
|
+
final_function_ids = function_ids
|
|
84
|
+
final_no_types = no_types
|
|
85
|
+
|
|
86
|
+
# cache the values used for this explicit generate command
|
|
87
|
+
cache_generate_args(
|
|
88
|
+
contexts=final_contexts,
|
|
89
|
+
names=final_names,
|
|
90
|
+
function_ids=final_function_ids,
|
|
91
|
+
no_types=final_no_types
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
generate(contexts=final_contexts, names=final_names, function_ids=final_function_ids, no_types=final_no_types)
|
|
54
95
|
|
|
55
96
|
generate_parser.set_defaults(command=generate_command)
|
|
56
97
|
|
polyapi/client.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import List, Dict, Any, TypedDict
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def _wrap_code_in_try_except(code: str) -> str:
|
|
13
|
+
def _wrap_code_in_try_except(function_name: str, code: str) -> str:
|
|
14
14
|
""" this is necessary because client functions with imports will blow up ALL server functions,
|
|
15
15
|
even if they don't use them.
|
|
16
16
|
because the server function will try to load all client functions when loading the library
|
|
@@ -18,8 +18,8 @@ def _wrap_code_in_try_except(code: str) -> str:
|
|
|
18
18
|
prefix = """logger = logging.getLogger("poly")
|
|
19
19
|
try:
|
|
20
20
|
"""
|
|
21
|
-
suffix = """except ImportError as e:
|
|
22
|
-
logger.
|
|
21
|
+
suffix = f"""except ImportError as e:
|
|
22
|
+
logger.warning("Failed to import client function '{function_name}', function unavailable: " + str(e))"""
|
|
23
23
|
|
|
24
24
|
lines = code.split("\n")
|
|
25
25
|
code = "\n ".join(lines)
|
|
@@ -39,6 +39,6 @@ def render_client_function(
|
|
|
39
39
|
return_type_def=return_type_def,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
code = _wrap_code_in_try_except(code)
|
|
42
|
+
code = _wrap_code_in_try_except(function_name, code)
|
|
43
43
|
|
|
44
44
|
return code + "\n\n", func_type_defs
|
polyapi/config.py
CHANGED
|
@@ -12,6 +12,10 @@ API_FUNCTION_DIRECT_EXECUTE = None
|
|
|
12
12
|
MTLS_CERT_PATH = None
|
|
13
13
|
MTLS_KEY_PATH = None
|
|
14
14
|
MTLS_CA_PATH = None
|
|
15
|
+
LAST_GENERATE_CONTEXTS = None
|
|
16
|
+
LAST_GENERATE_NAMES = None
|
|
17
|
+
LAST_GENERATE_FUNCTION_IDS = None
|
|
18
|
+
LAST_GENERATE_NO_TYPES = None
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
def get_config_file_path() -> str:
|
|
@@ -55,6 +59,16 @@ def get_api_key_and_url() -> Tuple[str | None, str | None]:
|
|
|
55
59
|
MTLS_CERT_PATH = config.get("polyapi", "mtls_cert_path", fallback=None)
|
|
56
60
|
MTLS_KEY_PATH = config.get("polyapi", "mtls_key_path", fallback=None)
|
|
57
61
|
MTLS_CA_PATH = config.get("polyapi", "mtls_ca_path", fallback=None)
|
|
62
|
+
|
|
63
|
+
# Read and cache generate command arguments
|
|
64
|
+
global LAST_GENERATE_CONTEXTS, LAST_GENERATE_NAMES, LAST_GENERATE_FUNCTION_IDS, LAST_GENERATE_NO_TYPES
|
|
65
|
+
contexts_str = config.get("polyapi", "last_generate_contexts_used", fallback=None)
|
|
66
|
+
LAST_GENERATE_CONTEXTS = contexts_str.split(",") if contexts_str else None
|
|
67
|
+
names_str = config.get("polyapi", "last_generate_names_used", fallback=None)
|
|
68
|
+
LAST_GENERATE_NAMES = names_str.split(",") if names_str else None
|
|
69
|
+
function_ids_str = config.get("polyapi", "last_generate_function_ids_used", fallback=None)
|
|
70
|
+
LAST_GENERATE_FUNCTION_IDS = function_ids_str.split(",") if function_ids_str else None
|
|
71
|
+
LAST_GENERATE_NO_TYPES = config.get("polyapi", "last_generate_no_types_used", fallback="false").lower() == "true"
|
|
58
72
|
|
|
59
73
|
return key, url
|
|
60
74
|
|
|
@@ -133,4 +147,58 @@ def get_direct_execute_config() -> bool:
|
|
|
133
147
|
if API_FUNCTION_DIRECT_EXECUTE is None:
|
|
134
148
|
# Force a config read if value isn't cached
|
|
135
149
|
get_api_key_and_url()
|
|
136
|
-
return bool(API_FUNCTION_DIRECT_EXECUTE)
|
|
150
|
+
return bool(API_FUNCTION_DIRECT_EXECUTE)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_cached_generate_args() -> Tuple[list | None, list | None, list | None, bool]:
|
|
154
|
+
"""Return cached generate command arguments"""
|
|
155
|
+
global LAST_GENERATE_CONTEXTS, LAST_GENERATE_NAMES, LAST_GENERATE_FUNCTION_IDS, LAST_GENERATE_NO_TYPES
|
|
156
|
+
if LAST_GENERATE_CONTEXTS is None and LAST_GENERATE_NAMES is None and LAST_GENERATE_FUNCTION_IDS is None and LAST_GENERATE_NO_TYPES is None:
|
|
157
|
+
# Force a config read if values aren't cached
|
|
158
|
+
get_api_key_and_url()
|
|
159
|
+
return LAST_GENERATE_CONTEXTS, LAST_GENERATE_NAMES, LAST_GENERATE_FUNCTION_IDS, bool(LAST_GENERATE_NO_TYPES)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cache_generate_args(contexts: list | None = None, names: list | None = None, function_ids: list | None = None, no_types: bool = False):
|
|
163
|
+
"""Cache generate command arguments to config file"""
|
|
164
|
+
from typing import List
|
|
165
|
+
|
|
166
|
+
# Read existing config
|
|
167
|
+
path = get_config_file_path()
|
|
168
|
+
config = configparser.ConfigParser()
|
|
169
|
+
|
|
170
|
+
if os.path.exists(path):
|
|
171
|
+
with open(path, "r") as f:
|
|
172
|
+
config.read_file(f)
|
|
173
|
+
|
|
174
|
+
# Ensure polyapi section exists
|
|
175
|
+
if "polyapi" not in config:
|
|
176
|
+
config["polyapi"] = {}
|
|
177
|
+
|
|
178
|
+
# Update cached values
|
|
179
|
+
global LAST_GENERATE_CONTEXTS, LAST_GENERATE_NAMES, LAST_GENERATE_FUNCTION_IDS, LAST_GENERATE_NO_TYPES
|
|
180
|
+
LAST_GENERATE_CONTEXTS = contexts
|
|
181
|
+
LAST_GENERATE_NAMES = names
|
|
182
|
+
LAST_GENERATE_FUNCTION_IDS = function_ids
|
|
183
|
+
LAST_GENERATE_NO_TYPES = no_types
|
|
184
|
+
|
|
185
|
+
# Write values to config
|
|
186
|
+
if contexts is not None:
|
|
187
|
+
config.set("polyapi", "last_generate_contexts_used", ",".join(contexts))
|
|
188
|
+
elif config.has_option("polyapi", "last_generate_contexts_used"):
|
|
189
|
+
config.remove_option("polyapi", "last_generate_contexts_used")
|
|
190
|
+
|
|
191
|
+
if names is not None:
|
|
192
|
+
config.set("polyapi", "last_generate_names_used", ",".join(names))
|
|
193
|
+
elif config.has_option("polyapi", "last_generate_names_used"):
|
|
194
|
+
config.remove_option("polyapi", "last_generate_names_used")
|
|
195
|
+
|
|
196
|
+
if function_ids is not None:
|
|
197
|
+
config.set("polyapi", "last_generate_function_ids_used", ",".join(function_ids))
|
|
198
|
+
elif config.has_option("polyapi", "last_generate_function_ids_used"):
|
|
199
|
+
config.remove_option("polyapi", "last_generate_function_ids_used")
|
|
200
|
+
|
|
201
|
+
config.set("polyapi", "last_generate_no_types_used", str(no_types).lower())
|
|
202
|
+
|
|
203
|
+
with open(path, "w") as f:
|
|
204
|
+
config.write(f)
|
polyapi/deployables.py
CHANGED
|
@@ -31,6 +31,7 @@ class ParsedDeployableConfig(TypedDict):
|
|
|
31
31
|
context: str
|
|
32
32
|
name: str
|
|
33
33
|
type: DeployableTypes
|
|
34
|
+
description: Optional[str]
|
|
34
35
|
disableAi: Optional[bool]
|
|
35
36
|
config: Dict[str, Any]
|
|
36
37
|
|
|
@@ -112,20 +113,22 @@ class PolyDeployConfig(TypedDict):
|
|
|
112
113
|
|
|
113
114
|
def get_all_deployable_files_windows(config: PolyDeployConfig) -> List[str]:
|
|
114
115
|
# Constructing the Windows command using dir and findstr
|
|
115
|
-
include_pattern = " ".join(f"*.{f}"
|
|
116
|
-
exclude_pattern = '
|
|
117
|
-
pattern = '
|
|
116
|
+
include_pattern = " ".join(f"*.{f}" for f in config["include_files_or_extensions"]) or "*"
|
|
117
|
+
exclude_pattern = ' '.join(f"\\{f}" for f in config["exclude_dirs"])
|
|
118
|
+
pattern = ' '.join(f"/C:\"polyConfig: {name}\"" for name in config["type_names"]) or '/C:"polyConfig"'
|
|
118
119
|
|
|
119
120
|
exclude_command = f" | findstr /V /I \"{exclude_pattern}\"" if exclude_pattern else ''
|
|
120
|
-
search_command = f" | findstr /M /I /F:/
|
|
121
|
+
search_command = f" | findstr /S /M /I /F:/ {pattern} *.*"
|
|
121
122
|
|
|
122
123
|
result = []
|
|
123
124
|
for dir_path in config["include_dirs"]:
|
|
124
|
-
|
|
125
|
+
if dir_path != '.':
|
|
126
|
+
include_pattern = " ".join(f"{dir_path}*.{f}" for f in config["include_files_or_extensions"]) or "*"
|
|
127
|
+
dir_command = f"dir {include_pattern} /S /P /B > NUL"
|
|
125
128
|
full_command = f"{dir_command}{exclude_command}{search_command}"
|
|
126
129
|
try:
|
|
127
130
|
output = subprocess.check_output(full_command, shell=True, text=True)
|
|
128
|
-
result.extend(output.strip().split('\
|
|
131
|
+
result.extend(output.strip().split('\n'))
|
|
129
132
|
except subprocess.CalledProcessError:
|
|
130
133
|
pass
|
|
131
134
|
return result
|
|
@@ -154,7 +157,7 @@ def get_all_deployable_files(config: PolyDeployConfig) -> List[str]:
|
|
|
154
157
|
if not config.get("include_files_or_extensions"):
|
|
155
158
|
config["include_files_or_extensions"] = ["py"]
|
|
156
159
|
if not config.get("exclude_dirs"):
|
|
157
|
-
config["exclude_dirs"] = ["
|
|
160
|
+
config["exclude_dirs"] = ["Lib", "node_modules", "dist", "build", "output", ".vscode", ".poly", ".github", ".husky", ".yarn", ".venv"]
|
|
158
161
|
|
|
159
162
|
is_windows = os.name == "nt"
|
|
160
163
|
if is_windows:
|
polyapi/function_cli.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from typing import Any, List, Optional
|
|
3
3
|
import requests
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
from polyapi.config import get_api_key_and_url
|
|
6
6
|
from polyapi.utils import get_auth_headers, print_green, print_red, print_yellow
|
|
7
7
|
from polyapi.parser import parse_function_code, get_jsonschema_type
|
|
@@ -91,7 +91,9 @@ def function_add_or_update(
|
|
|
91
91
|
function_id = resp.json()["id"]
|
|
92
92
|
print(f"Function ID: {function_id}")
|
|
93
93
|
if generate:
|
|
94
|
-
|
|
94
|
+
# Use cached generate arguments when regenerating after function deployment
|
|
95
|
+
from polyapi.generate import generate_from_cache
|
|
96
|
+
generate_from_cache()
|
|
95
97
|
else:
|
|
96
98
|
print("Error adding function.")
|
|
97
99
|
print(resp.status_code)
|
polyapi/generate.py
CHANGED
|
@@ -2,7 +2,9 @@ import json
|
|
|
2
2
|
import requests
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
|
-
|
|
5
|
+
import logging
|
|
6
|
+
import tempfile
|
|
7
|
+
from typing import Any, List, Optional, Tuple, cast
|
|
6
8
|
|
|
7
9
|
from .auth import render_auth_function
|
|
8
10
|
from .client import render_client_function
|
|
@@ -14,7 +16,7 @@ from .api import render_api_function
|
|
|
14
16
|
from .server import render_server_function
|
|
15
17
|
from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace
|
|
16
18
|
from .variables import generate_variables
|
|
17
|
-
from .config import get_api_key_and_url, get_direct_execute_config
|
|
19
|
+
from .config import get_api_key_and_url, get_direct_execute_config, get_cached_generate_args
|
|
18
20
|
|
|
19
21
|
SUPPORTED_FUNCTION_TYPES = {
|
|
20
22
|
"apiFunction",
|
|
@@ -36,15 +38,21 @@ Unresolved schema, please add the following schema to complete it:
|
|
|
36
38
|
path:'''
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
def get_specs(contexts=Optional[List[str]], no_types: bool = False) -> List:
|
|
41
|
+
def get_specs(contexts: Optional[List[str]] = None, names: Optional[List[str]] = None, function_ids: Optional[List[str]] = None, no_types: bool = False) -> List:
|
|
40
42
|
api_key, api_url = get_api_key_and_url()
|
|
41
43
|
assert api_key
|
|
42
44
|
headers = get_auth_headers(api_key)
|
|
43
45
|
url = f"{api_url}/specs"
|
|
44
|
-
params = {"noTypes": str(no_types).lower()}
|
|
46
|
+
params: Any = {"noTypes": str(no_types).lower()}
|
|
45
47
|
|
|
46
48
|
if contexts:
|
|
47
49
|
params["contexts"] = contexts
|
|
50
|
+
|
|
51
|
+
if names:
|
|
52
|
+
params["names"] = names
|
|
53
|
+
|
|
54
|
+
if function_ids:
|
|
55
|
+
params["functionIds"] = function_ids
|
|
48
56
|
|
|
49
57
|
# Add apiFunctionDirectExecute parameter if direct execute is enabled
|
|
50
58
|
if get_direct_execute_config():
|
|
@@ -149,7 +157,7 @@ def parse_function_specs(
|
|
|
149
157
|
|
|
150
158
|
# Functions with serverSideAsync True will always return a Dict with execution ID
|
|
151
159
|
if spec.get('serverSideAsync') and spec.get("function"):
|
|
152
|
-
spec['function']['returnType'] = {'kind': 'plain', 'value': 'object'}
|
|
160
|
+
spec['function']['returnType'] = {'kind': 'plain', 'value': 'object'} # type: ignore
|
|
153
161
|
|
|
154
162
|
functions.append(spec)
|
|
155
163
|
|
|
@@ -264,12 +272,26 @@ sys.modules[__name__] = _SchemaModule()
|
|
|
264
272
|
''')
|
|
265
273
|
|
|
266
274
|
|
|
267
|
-
def
|
|
275
|
+
def generate_from_cache() -> None:
|
|
276
|
+
"""
|
|
277
|
+
Generate using cached values after non-explicit call.
|
|
278
|
+
"""
|
|
279
|
+
cached_contexts, cached_names, cached_function_ids, cached_no_types = get_cached_generate_args()
|
|
280
|
+
|
|
281
|
+
generate(
|
|
282
|
+
contexts=cached_contexts,
|
|
283
|
+
names=cached_names,
|
|
284
|
+
function_ids=cached_function_ids,
|
|
285
|
+
no_types=cached_no_types
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def generate(contexts: Optional[List[str]] = None, names: Optional[List[str]] = None, function_ids: Optional[List[str]] = None, no_types: bool = False) -> None:
|
|
268
290
|
generate_msg = f"Generating Poly Python SDK for contexts ${contexts}..." if contexts else "Generating Poly Python SDK..."
|
|
269
291
|
print(generate_msg, end="", flush=True)
|
|
270
292
|
remove_old_library()
|
|
271
293
|
|
|
272
|
-
specs = get_specs(
|
|
294
|
+
specs = get_specs(contexts=contexts, names=names, function_ids=function_ids, no_types=no_types)
|
|
273
295
|
cache_specs(specs)
|
|
274
296
|
|
|
275
297
|
limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
|
|
@@ -301,11 +323,9 @@ def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> No
|
|
|
301
323
|
)
|
|
302
324
|
exit()
|
|
303
325
|
|
|
304
|
-
|
|
305
|
-
if
|
|
306
|
-
variables
|
|
307
|
-
if variables:
|
|
308
|
-
generate_variables(variables)
|
|
326
|
+
variables = get_variables()
|
|
327
|
+
if variables:
|
|
328
|
+
generate_variables(variables)
|
|
309
329
|
|
|
310
330
|
# indicator to vscode extension that this is a polyapi-python project
|
|
311
331
|
file_path = os.path.join(os.getcwd(), ".polyapi-python")
|
|
@@ -334,8 +354,9 @@ def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
|
|
|
334
354
|
function_id = spec["id"]
|
|
335
355
|
|
|
336
356
|
arguments: List[PropertySpecification] = []
|
|
337
|
-
return_type = {}
|
|
357
|
+
return_type: Any = {}
|
|
338
358
|
if spec.get("function"):
|
|
359
|
+
assert spec["function"]
|
|
339
360
|
# Handle cases where arguments might be missing or None
|
|
340
361
|
if spec["function"].get("arguments"):
|
|
341
362
|
arguments = [
|
|
@@ -407,48 +428,124 @@ def add_function_file(
|
|
|
407
428
|
function_name: str,
|
|
408
429
|
spec: SpecificationDto,
|
|
409
430
|
):
|
|
410
|
-
|
|
411
|
-
|
|
431
|
+
"""
|
|
432
|
+
Atomically add a function file to prevent partial corruption during generation failures.
|
|
433
|
+
|
|
434
|
+
This function generates all content first, then writes files atomically using temporary files
|
|
435
|
+
to ensure that either the entire operation succeeds or no changes are made to the filesystem.
|
|
436
|
+
"""
|
|
437
|
+
try:
|
|
438
|
+
# first lets add the import to the __init__
|
|
439
|
+
init_the_init(full_path)
|
|
412
440
|
|
|
413
|
-
|
|
441
|
+
func_str, func_type_defs = render_spec(spec)
|
|
414
442
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
with open(init_path, "a") as f:
|
|
419
|
-
f.write(f"\n\nfrom . import {to_func_namespace(function_name)}\n\n{func_str}")
|
|
443
|
+
if not func_str:
|
|
444
|
+
# If render_spec failed and returned empty string, don't create any files
|
|
445
|
+
raise Exception("Function rendering failed - empty function string returned")
|
|
420
446
|
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
447
|
+
# Prepare all content first before writing any files
|
|
448
|
+
func_namespace = to_func_namespace(function_name)
|
|
449
|
+
init_path = os.path.join(full_path, "__init__.py")
|
|
450
|
+
func_file_path = os.path.join(full_path, f"{func_namespace}.py")
|
|
451
|
+
|
|
452
|
+
# Read current __init__.py content if it exists
|
|
453
|
+
init_content = ""
|
|
454
|
+
if os.path.exists(init_path):
|
|
455
|
+
with open(init_path, "r") as f:
|
|
456
|
+
init_content = f.read()
|
|
457
|
+
|
|
458
|
+
# Prepare new content to append to __init__.py
|
|
459
|
+
new_init_content = init_content + f"\n\nfrom . import {func_namespace}\n\n{func_str}"
|
|
460
|
+
|
|
461
|
+
# Use temporary files for atomic writes
|
|
462
|
+
# Write to __init__.py atomically
|
|
463
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_init:
|
|
464
|
+
temp_init.write(new_init_content)
|
|
465
|
+
temp_init_path = temp_init.name
|
|
466
|
+
|
|
467
|
+
# Write to function file atomically
|
|
468
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_func:
|
|
469
|
+
temp_func.write(func_type_defs)
|
|
470
|
+
temp_func_path = temp_func.name
|
|
471
|
+
|
|
472
|
+
# Atomic operations: move temp files to final locations
|
|
473
|
+
shutil.move(temp_init_path, init_path)
|
|
474
|
+
shutil.move(temp_func_path, func_file_path)
|
|
475
|
+
|
|
476
|
+
except Exception as e:
|
|
477
|
+
# Clean up any temporary files that might have been created
|
|
478
|
+
try:
|
|
479
|
+
if 'temp_init_path' in locals() and os.path.exists(temp_init_path):
|
|
480
|
+
os.unlink(temp_init_path)
|
|
481
|
+
if 'temp_func_path' in locals() and os.path.exists(temp_func_path):
|
|
482
|
+
os.unlink(temp_func_path)
|
|
483
|
+
except:
|
|
484
|
+
pass # Best effort cleanup
|
|
485
|
+
|
|
486
|
+
# Re-raise the original exception
|
|
487
|
+
raise e
|
|
425
488
|
|
|
426
489
|
|
|
427
490
|
def create_function(
|
|
428
491
|
spec: SpecificationDto
|
|
429
492
|
) -> None:
|
|
493
|
+
"""
|
|
494
|
+
Create a function with atomic directory and file operations.
|
|
495
|
+
|
|
496
|
+
Tracks directory creation to enable cleanup on failure.
|
|
497
|
+
"""
|
|
430
498
|
full_path = os.path.dirname(os.path.abspath(__file__))
|
|
431
499
|
folders = f"poly.{spec['context']}.{spec['name']}".split(".")
|
|
432
|
-
for
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
500
|
+
created_dirs = [] # Track directories we create for cleanup on failure
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
for idx, folder in enumerate(folders):
|
|
504
|
+
if idx + 1 == len(folders):
|
|
505
|
+
# special handling for final level
|
|
506
|
+
add_function_file(
|
|
507
|
+
full_path,
|
|
508
|
+
folder,
|
|
509
|
+
spec,
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
full_path = os.path.join(full_path, folder)
|
|
513
|
+
if not os.path.exists(full_path):
|
|
514
|
+
os.makedirs(full_path)
|
|
515
|
+
created_dirs.append(full_path) # Track for cleanup
|
|
516
|
+
|
|
517
|
+
# append to __init__.py file if nested folders
|
|
518
|
+
next = folders[idx + 1] if idx + 2 < len(folders) else ""
|
|
519
|
+
if next:
|
|
520
|
+
init_the_init(full_path)
|
|
521
|
+
add_import_to_init(full_path, next)
|
|
522
|
+
|
|
523
|
+
except Exception as e:
|
|
524
|
+
# Clean up directories we created (in reverse order)
|
|
525
|
+
for dir_path in reversed(created_dirs):
|
|
526
|
+
try:
|
|
527
|
+
if os.path.exists(dir_path) and not os.listdir(dir_path): # Only remove if empty
|
|
528
|
+
os.rmdir(dir_path)
|
|
529
|
+
except:
|
|
530
|
+
pass # Best effort cleanup
|
|
531
|
+
|
|
532
|
+
# Re-raise the original exception
|
|
533
|
+
raise e
|
|
450
534
|
|
|
451
535
|
|
|
452
536
|
def generate_functions(functions: List[SpecificationDto]) -> None:
|
|
537
|
+
failed_functions = []
|
|
453
538
|
for func in functions:
|
|
454
|
-
|
|
539
|
+
try:
|
|
540
|
+
create_function(func)
|
|
541
|
+
except Exception as e:
|
|
542
|
+
function_path = f"{func.get('context', 'unknown')}.{func.get('name', 'unknown')}"
|
|
543
|
+
function_id = func.get('id', 'unknown')
|
|
544
|
+
failed_functions.append(f"{function_path} (id: {function_id})")
|
|
545
|
+
logging.warning(f"WARNING: Failed to generate function {function_path} (id: {function_id}): {str(e)}")
|
|
546
|
+
continue
|
|
547
|
+
|
|
548
|
+
if failed_functions:
|
|
549
|
+
logging.warning(f"WARNING: {len(failed_functions)} function(s) failed to generate:")
|
|
550
|
+
for failed_func in failed_functions:
|
|
551
|
+
logging.warning(f" - {failed_func}")
|
polyapi/parser.py
CHANGED
|
@@ -513,7 +513,13 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
513
513
|
deployable["context"] = context or deployable["config"].get("context", "")
|
|
514
514
|
deployable["name"] = name or deployable["config"].get("name", "")
|
|
515
515
|
deployable["disableAi"] = deployable["config"].get("disableAi", False)
|
|
516
|
-
deployable["description"] = deployable["
|
|
516
|
+
deployable["description"] = deployable["config"].get("description", "")
|
|
517
|
+
if deployable["description"]:
|
|
518
|
+
if deployable["description"] != deployable["types"].get("description", ""):
|
|
519
|
+
deployable["types"]["description"] = deployable["description"]
|
|
520
|
+
deployable["dirty"] = True
|
|
521
|
+
else:
|
|
522
|
+
deployable["description"] = deployable["types"].get("description", "")
|
|
517
523
|
if not deployable["name"]:
|
|
518
524
|
print_red("ERROR")
|
|
519
525
|
print("Function config is missing a name.")
|
polyapi/poly_schemas.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import logging
|
|
3
|
+
import tempfile
|
|
4
|
+
import shutil
|
|
2
5
|
from typing import Any, Dict, List, Tuple
|
|
3
6
|
|
|
4
7
|
from polyapi.schema import wrapped_generate_schema_types
|
|
@@ -21,13 +24,67 @@ FALLBACK_SPEC_TEMPLATE = """class {name}(TypedDict, total=False):
|
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
def generate_schemas(specs: List[SchemaSpecDto], limit_ids: List[str] = None):
|
|
27
|
+
failed_schemas = []
|
|
28
|
+
successful_schemas = []
|
|
24
29
|
if limit_ids:
|
|
25
30
|
for spec in specs:
|
|
26
31
|
if spec["id"] in limit_ids:
|
|
27
|
-
|
|
32
|
+
try:
|
|
33
|
+
create_schema(spec)
|
|
34
|
+
successful_schemas.append(f"{spec.get('context', 'unknown')}.{spec.get('name', 'unknown')}")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
schema_path = f"{spec.get('context', 'unknown')}.{spec.get('name', 'unknown')}"
|
|
37
|
+
schema_id = spec.get('id', 'unknown')
|
|
38
|
+
failed_schemas.append(f"{schema_path} (id: {schema_id})")
|
|
39
|
+
logging.warning(f"WARNING: Failed to generate schema {schema_path} (id: {schema_id}): {str(e)}")
|
|
40
|
+
continue
|
|
28
41
|
else:
|
|
29
42
|
for spec in specs:
|
|
30
|
-
|
|
43
|
+
try:
|
|
44
|
+
create_schema(spec)
|
|
45
|
+
successful_schemas.append(f"{spec.get('context', 'unknown')}.{spec.get('name', 'unknown')}")
|
|
46
|
+
except Exception as e:
|
|
47
|
+
schema_path = f"{spec.get('context', 'unknown')}.{spec.get('name', 'unknown')}"
|
|
48
|
+
schema_id = spec.get('id', 'unknown')
|
|
49
|
+
failed_schemas.append(f"{schema_path} (id: {schema_id})")
|
|
50
|
+
logging.warning(f"WARNING: Failed to generate schema {schema_path} (id: {schema_id}): {str(e)}")
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if failed_schemas:
|
|
54
|
+
logging.warning(f"WARNING: {len(failed_schemas)} schema(s) failed to generate:")
|
|
55
|
+
for failed_schema in failed_schemas:
|
|
56
|
+
logging.warning(f" - {failed_schema}")
|
|
57
|
+
logging.warning(f"Successfully generated {len(successful_schemas)} schema(s)")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def validate_schema_content(schema_content: str, schema_name: str) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Validate that the schema content is meaningful and not just imports.
|
|
63
|
+
Returns True if the schema is valid, False otherwise.
|
|
64
|
+
"""
|
|
65
|
+
if not schema_content or not schema_content.strip():
|
|
66
|
+
logging.debug(f"Schema {schema_name} failed validation: Empty content")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
lines = schema_content.strip().split('\n')
|
|
70
|
+
|
|
71
|
+
# Check if the content has any actual class definitions or type aliases
|
|
72
|
+
has_class_definition = any(line.strip().startswith('class ') for line in lines)
|
|
73
|
+
has_type_alias = any(schema_name in line and '=' in line and not line.strip().startswith('#') for line in lines)
|
|
74
|
+
|
|
75
|
+
# Check if it's essentially just imports (less than 5 lines and no meaningful definitions)
|
|
76
|
+
meaningful_lines = [line for line in lines if line.strip() and not line.strip().startswith('from ') and not line.strip().startswith('import ') and not line.strip().startswith('#')]
|
|
77
|
+
|
|
78
|
+
# Enhanced logging for debugging
|
|
79
|
+
if not (has_class_definition or has_type_alias) or len(meaningful_lines) < 1:
|
|
80
|
+
# Determine the specific reason for failure
|
|
81
|
+
if len(meaningful_lines) == 0:
|
|
82
|
+
logging.debug(f"Schema {schema_name} failed validation: No meaningful content (only imports) - likely empty object or unresolved reference")
|
|
83
|
+
elif not has_class_definition and not has_type_alias:
|
|
84
|
+
logging.debug(f"Schema {schema_name} failed validation: No class definition or type alias found")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
return True
|
|
31
88
|
|
|
32
89
|
|
|
33
90
|
def add_schema_file(
|
|
@@ -35,51 +92,114 @@ def add_schema_file(
|
|
|
35
92
|
schema_name: str,
|
|
36
93
|
spec: SchemaSpecDto,
|
|
37
94
|
):
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
95
|
+
"""
|
|
96
|
+
Atomically add a schema file to prevent partial corruption during generation failures.
|
|
97
|
+
|
|
98
|
+
This function generates all content first, then writes files atomically using temporary files
|
|
99
|
+
to ensure that either the entire operation succeeds or no changes are made to the filesystem.
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
# first lets add the import to the __init__
|
|
103
|
+
init_the_init(full_path, SCHEMA_CODE_IMPORTS)
|
|
104
|
+
|
|
105
|
+
if not spec["definition"].get("title"):
|
|
106
|
+
# very empty schemas like mews.Unit are possible
|
|
107
|
+
# add a title here to be sure they render
|
|
108
|
+
spec["definition"]["title"] = schema_name
|
|
109
|
+
|
|
110
|
+
schema_defs = render_poly_schema(spec)
|
|
111
|
+
|
|
112
|
+
# Validate schema content before proceeding
|
|
113
|
+
if not validate_schema_content(schema_defs, schema_name):
|
|
114
|
+
raise Exception(f"Schema rendering failed or produced invalid content for {schema_name}")
|
|
115
|
+
|
|
116
|
+
# Prepare all content first before writing any files
|
|
117
|
+
schema_namespace = to_func_namespace(schema_name)
|
|
50
118
|
init_path = os.path.join(full_path, "__init__.py")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
119
|
+
schema_file_path = os.path.join(full_path, f"_{schema_namespace}.py")
|
|
120
|
+
|
|
121
|
+
# Read current __init__.py content if it exists
|
|
122
|
+
init_content = ""
|
|
123
|
+
if os.path.exists(init_path):
|
|
124
|
+
with open(init_path, "r") as f:
|
|
125
|
+
init_content = f.read()
|
|
126
|
+
|
|
127
|
+
# Prepare new content to append to __init__.py
|
|
128
|
+
new_init_content = init_content + f"\n\nfrom ._{schema_namespace} import {schema_name}\n__all__.append('{schema_name}')\n"
|
|
129
|
+
|
|
130
|
+
# Use temporary files for atomic writes
|
|
131
|
+
# Write to __init__.py atomically
|
|
132
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_init:
|
|
133
|
+
temp_init.write(new_init_content)
|
|
134
|
+
temp_init_path = temp_init.name
|
|
135
|
+
|
|
136
|
+
# Write to schema file atomically
|
|
137
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_schema:
|
|
138
|
+
temp_schema.write(schema_defs)
|
|
139
|
+
temp_schema_path = temp_schema.name
|
|
140
|
+
|
|
141
|
+
# Atomic operations: move temp files to final locations
|
|
142
|
+
shutil.move(temp_init_path, init_path)
|
|
143
|
+
shutil.move(temp_schema_path, schema_file_path)
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
# Clean up any temporary files that might have been created
|
|
147
|
+
try:
|
|
148
|
+
if 'temp_init_path' in locals() and os.path.exists(temp_init_path):
|
|
149
|
+
os.unlink(temp_init_path)
|
|
150
|
+
if 'temp_schema_path' in locals() and os.path.exists(temp_schema_path):
|
|
151
|
+
os.unlink(temp_schema_path)
|
|
152
|
+
except:
|
|
153
|
+
pass # Best effort cleanup
|
|
154
|
+
|
|
155
|
+
# Re-raise the original exception
|
|
156
|
+
raise e
|
|
58
157
|
|
|
59
158
|
|
|
60
159
|
def create_schema(
|
|
61
160
|
spec: SchemaSpecDto
|
|
62
161
|
) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Create a schema with atomic directory and file operations.
|
|
164
|
+
|
|
165
|
+
Tracks directory creation to enable cleanup on failure.
|
|
166
|
+
"""
|
|
63
167
|
full_path = os.path.dirname(os.path.abspath(__file__))
|
|
64
168
|
folders = f"schemas.{spec['context']}.{spec['name']}".split(".")
|
|
65
|
-
for
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
169
|
+
created_dirs = [] # Track directories we create for cleanup on failure
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
for idx, folder in enumerate(folders):
|
|
173
|
+
if idx + 1 == len(folders):
|
|
174
|
+
# special handling for final level
|
|
175
|
+
add_schema_file(
|
|
176
|
+
full_path,
|
|
177
|
+
folder,
|
|
178
|
+
spec,
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
full_path = os.path.join(full_path, folder)
|
|
182
|
+
if not os.path.exists(full_path):
|
|
183
|
+
os.makedirs(full_path)
|
|
184
|
+
created_dirs.append(full_path) # Track for cleanup
|
|
185
|
+
|
|
186
|
+
# append to __init__.py file if nested folders
|
|
187
|
+
next = folders[idx + 1] if idx + 2 < len(folders) else ""
|
|
188
|
+
if next:
|
|
189
|
+
init_the_init(full_path, SCHEMA_CODE_IMPORTS)
|
|
190
|
+
add_import_to_init(full_path, next)
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
# Clean up directories we created (in reverse order)
|
|
194
|
+
for dir_path in reversed(created_dirs):
|
|
195
|
+
try:
|
|
196
|
+
if os.path.exists(dir_path) and not os.listdir(dir_path): # Only remove if empty
|
|
197
|
+
os.rmdir(dir_path)
|
|
198
|
+
except:
|
|
199
|
+
pass # Best effort cleanup
|
|
200
|
+
|
|
201
|
+
# Re-raise the original exception
|
|
202
|
+
raise e
|
|
83
203
|
|
|
84
204
|
|
|
85
205
|
def add_schema_to_init(full_path: str, spec: SchemaSpecDto):
|
polyapi/prepare.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
+
import subprocess
|
|
3
4
|
from typing import List, Tuple, Literal
|
|
4
5
|
import requests
|
|
5
6
|
|
|
@@ -31,7 +32,7 @@ def get_server_function_description(description: str, arguments, code: str) -> s
|
|
|
31
32
|
api_key, api_url = get_api_key_and_url()
|
|
32
33
|
headers = get_auth_headers(api_key)
|
|
33
34
|
data = {"description": description, "arguments": arguments, "code": code}
|
|
34
|
-
response = requests.post(f"{api_url}/server-
|
|
35
|
+
response = requests.post(f"{api_url}/functions/server/description-generation", headers=headers, json=data)
|
|
35
36
|
return response.json()
|
|
36
37
|
|
|
37
38
|
def get_client_function_description(description: str, arguments, code: str) -> str:
|
|
@@ -39,7 +40,7 @@ def get_client_function_description(description: str, arguments, code: str) -> s
|
|
|
39
40
|
headers = get_auth_headers(api_key)
|
|
40
41
|
# Simulated API call to generate client function descriptions
|
|
41
42
|
data = {"description": description, "arguments": arguments, "code": code}
|
|
42
|
-
response = requests.post(f"{api_url}/client-
|
|
43
|
+
response = requests.post(f"{api_url}/functions/client/description-generation", headers=headers, json=data)
|
|
43
44
|
return response.json()
|
|
44
45
|
|
|
45
46
|
def fill_in_missing_function_details(deployable: DeployableRecord, code: str) -> DeployableRecord:
|
|
@@ -135,6 +136,16 @@ def prepare_deployables(lazy: bool = False, disable_docs: bool = False, disable_
|
|
|
135
136
|
# NOTE: write_updated_deployable has side effects that update deployable.fileRevision which is in both this list and parsed_deployables
|
|
136
137
|
for deployable in dirty_deployables:
|
|
137
138
|
write_updated_deployable(deployable, disable_docs)
|
|
139
|
+
# Re-stage any updated staged files.
|
|
140
|
+
staged = subprocess.check_output('git diff --name-only --cached', shell=True, text=True, ).split('\n')
|
|
141
|
+
for deployable in dirty_deployables:
|
|
142
|
+
try:
|
|
143
|
+
if deployable["file"] in staged:
|
|
144
|
+
print(f'Staging {deployable["file"]}')
|
|
145
|
+
subprocess.run(['git', 'add', deployable["file"]])
|
|
146
|
+
except:
|
|
147
|
+
print('Warning: File staging failed, check that all files are staged properly.')
|
|
148
|
+
|
|
138
149
|
|
|
139
150
|
print("Poly deployments are prepared.")
|
|
140
151
|
save_deployable_records(parsed_deployables)
|
polyapi/schema.py
CHANGED
|
@@ -64,7 +64,7 @@ def wrapped_generate_schema_types(type_spec: dict, root, fallback_type):
|
|
|
64
64
|
# {'$ref': '#/definitions/FinanceAccountListModel'}
|
|
65
65
|
return fallback_type, ""
|
|
66
66
|
except:
|
|
67
|
-
logging.
|
|
67
|
+
logging.warning(f"WARNING parsing jsonschema failed: {type_spec}\nusing fallback type '{fallback_type}'")
|
|
68
68
|
return fallback_type, ""
|
|
69
69
|
|
|
70
70
|
|
|
@@ -104,12 +104,26 @@ def generate_schema_types(input_data: Dict, root=None):
|
|
|
104
104
|
# Regex to match everything between "# example: {\n" and "^}$"
|
|
105
105
|
MALFORMED_EXAMPLES_PATTERN = re.compile(r"# example: \{\n.*?^\}$", flags=re.DOTALL | re.MULTILINE)
|
|
106
106
|
|
|
107
|
+
# Regex to fix invalid escape sequences in docstrings
|
|
108
|
+
INVALID_ESCAPE_PATTERNS = [
|
|
109
|
+
# Fix "\ " (backslash space) which is not a valid escape sequence
|
|
110
|
+
(re.compile(r'\\(\s)', re.DOTALL), r'\1'),
|
|
111
|
+
# Fix other common invalid escape sequences in docstrings
|
|
112
|
+
(re.compile(r'\\([^nrtbfav"\'\\])', re.DOTALL), r'\\\\\1'),
|
|
113
|
+
]
|
|
114
|
+
|
|
107
115
|
|
|
108
116
|
def clean_malformed_examples(example: str) -> str:
|
|
109
117
|
""" there is a bug in the `jsonschmea_gentypes` library where if an example from a jsonchema is an object,
|
|
110
|
-
it will break the code because the object won't be properly commented out
|
|
118
|
+
it will break the code because the object won't be properly commented out. Also fixes invalid escape sequences.
|
|
111
119
|
"""
|
|
120
|
+
# Remove malformed examples
|
|
112
121
|
cleaned_example = MALFORMED_EXAMPLES_PATTERN.sub("", example)
|
|
122
|
+
|
|
123
|
+
# Fix invalid escape sequences in docstrings
|
|
124
|
+
for pattern, replacement in INVALID_ESCAPE_PATTERNS:
|
|
125
|
+
cleaned_example = pattern.sub(replacement, cleaned_example)
|
|
126
|
+
|
|
113
127
|
return cleaned_example
|
|
114
128
|
|
|
115
129
|
|
polyapi/typedefs.py
CHANGED
polyapi/variables.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import logging
|
|
3
|
+
import tempfile
|
|
4
|
+
import shutil
|
|
2
5
|
from typing import List
|
|
3
6
|
|
|
4
7
|
from polyapi.schema import map_primitive_types
|
|
@@ -70,8 +73,21 @@ class {variable_name}:{get_method}
|
|
|
70
73
|
|
|
71
74
|
|
|
72
75
|
def generate_variables(variables: List[VariableSpecDto]):
|
|
76
|
+
failed_variables = []
|
|
73
77
|
for variable in variables:
|
|
74
|
-
|
|
78
|
+
try:
|
|
79
|
+
create_variable(variable)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
variable_path = f"{variable.get('context', 'unknown')}.{variable.get('name', 'unknown')}"
|
|
82
|
+
variable_id = variable.get('id', 'unknown')
|
|
83
|
+
failed_variables.append(f"{variable_path} (id: {variable_id})")
|
|
84
|
+
logging.warning(f"WARNING: Failed to generate variable {variable_path} (id: {variable_id}): {str(e)}")
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if failed_variables:
|
|
88
|
+
logging.warning(f"WARNING: {len(failed_variables)} variable(s) failed to generate:")
|
|
89
|
+
for failed_var in failed_variables:
|
|
90
|
+
logging.warning(f" - {failed_var}")
|
|
75
91
|
|
|
76
92
|
|
|
77
93
|
def render_variable(variable: VariableSpecDto):
|
|
@@ -116,26 +132,84 @@ def _get_variable_type(type_spec: PropertyType) -> str:
|
|
|
116
132
|
|
|
117
133
|
|
|
118
134
|
def create_variable(variable: VariableSpecDto) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Create a variable with atomic directory and file operations.
|
|
137
|
+
|
|
138
|
+
Tracks directory creation to enable cleanup on failure.
|
|
139
|
+
"""
|
|
119
140
|
folders = ["vari"]
|
|
120
141
|
if variable["context"]:
|
|
121
142
|
folders += variable["context"].split(".")
|
|
122
143
|
|
|
123
144
|
# build up the full_path by adding all the folders
|
|
124
145
|
full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
os.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
created_dirs = [] # Track directories we create for cleanup on failure
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
for idx, folder in enumerate(folders):
|
|
150
|
+
full_path = os.path.join(full_path, folder)
|
|
151
|
+
if not os.path.exists(full_path):
|
|
152
|
+
os.makedirs(full_path)
|
|
153
|
+
created_dirs.append(full_path) # Track for cleanup
|
|
154
|
+
next = folders[idx + 1] if idx + 1 < len(folders) else None
|
|
155
|
+
if next:
|
|
156
|
+
add_import_to_init(full_path, next)
|
|
157
|
+
|
|
158
|
+
add_variable_to_init(full_path, variable)
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
# Clean up directories we created (in reverse order)
|
|
162
|
+
for dir_path in reversed(created_dirs):
|
|
163
|
+
try:
|
|
164
|
+
if os.path.exists(dir_path) and not os.listdir(dir_path): # Only remove if empty
|
|
165
|
+
os.rmdir(dir_path)
|
|
166
|
+
except:
|
|
167
|
+
pass # Best effort cleanup
|
|
168
|
+
|
|
169
|
+
# Re-raise the original exception
|
|
170
|
+
raise e
|
|
135
171
|
|
|
136
172
|
|
|
137
173
|
def add_variable_to_init(full_path: str, variable: VariableSpecDto):
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
174
|
+
"""
|
|
175
|
+
Atomically add a variable to __init__.py to prevent partial corruption during generation failures.
|
|
176
|
+
|
|
177
|
+
This function generates all content first, then writes the file atomically using temporary files
|
|
178
|
+
to ensure that either the entire operation succeeds or no changes are made to the filesystem.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
init_the_init(full_path)
|
|
182
|
+
init_path = os.path.join(full_path, "__init__.py")
|
|
183
|
+
|
|
184
|
+
# Generate variable content first
|
|
185
|
+
variable_content = render_variable(variable)
|
|
186
|
+
if not variable_content:
|
|
187
|
+
raise Exception("Variable rendering failed - empty content returned")
|
|
188
|
+
|
|
189
|
+
# Read current __init__.py content if it exists
|
|
190
|
+
init_content = ""
|
|
191
|
+
if os.path.exists(init_path):
|
|
192
|
+
with open(init_path, "r") as f:
|
|
193
|
+
init_content = f.read()
|
|
194
|
+
|
|
195
|
+
# Prepare new content to append
|
|
196
|
+
new_init_content = init_content + variable_content + "\n\n"
|
|
197
|
+
|
|
198
|
+
# Write to temporary file first, then atomic move
|
|
199
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_file:
|
|
200
|
+
temp_file.write(new_init_content)
|
|
201
|
+
temp_file_path = temp_file.name
|
|
202
|
+
|
|
203
|
+
# Atomic operation: move temp file to final location
|
|
204
|
+
shutil.move(temp_file_path, init_path)
|
|
205
|
+
|
|
206
|
+
except Exception as e:
|
|
207
|
+
# Clean up temporary file if it exists
|
|
208
|
+
try:
|
|
209
|
+
if 'temp_file_path' in locals() and os.path.exists(temp_file_path):
|
|
210
|
+
os.unlink(temp_file_path)
|
|
211
|
+
except:
|
|
212
|
+
pass # Best effort cleanup
|
|
213
|
+
|
|
214
|
+
# Re-raise the original exception
|
|
215
|
+
raise e
|
polyapi/webhook.py
CHANGED
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import socketio # type: ignore
|
|
3
3
|
from socketio.exceptions import ConnectionError # type: ignore
|
|
4
4
|
import uuid
|
|
5
|
+
import logging
|
|
5
6
|
from typing import Any, Dict, List, Tuple
|
|
6
7
|
|
|
7
8
|
from polyapi.config import get_api_key_and_url
|
|
@@ -121,22 +122,27 @@ def render_webhook_handle(
|
|
|
121
122
|
arguments: List[PropertySpecification],
|
|
122
123
|
return_type: Dict[str, Any],
|
|
123
124
|
) -> Tuple[str, str]:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
125
|
+
try:
|
|
126
|
+
function_args, function_args_def = parse_arguments(function_name, arguments)
|
|
127
|
+
|
|
128
|
+
if "WebhookEventType" in function_args:
|
|
129
|
+
# let's add the function name import!
|
|
130
|
+
function_args = function_args.replace("WebhookEventType", f"{to_func_namespace(function_name)}.WebhookEventType")
|
|
131
|
+
|
|
132
|
+
func_str = WEBHOOK_TEMPLATE.format(
|
|
133
|
+
description=function_description,
|
|
134
|
+
client_id=uuid.uuid4().hex,
|
|
135
|
+
function_id=function_id,
|
|
136
|
+
function_name=function_name,
|
|
137
|
+
function_args=function_args,
|
|
138
|
+
function_path=poly_full_path(function_context, function_name),
|
|
139
|
+
)
|
|
140
|
+
func_defs = WEBHOOK_DEFS_TEMPLATE.format(function_args_def=function_args_def)
|
|
141
|
+
return func_str, func_defs
|
|
142
|
+
except Exception as e:
|
|
143
|
+
logging.warning(f"Failed to render webhook handle {function_context}.{function_name} (id: {function_id}): {str(e)}")
|
|
144
|
+
# Return empty strings to indicate generation failure - this will be caught by generate_functions error handling
|
|
145
|
+
return "", ""
|
|
140
146
|
|
|
141
147
|
|
|
142
148
|
def start(*args):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: The Python Client for PolyAPI, the IPaaS by Developers for Developers
|
|
5
5
|
Author-email: Dan Fellin <dan@polyapi.io>
|
|
6
6
|
License: MIT License
|
|
@@ -31,7 +31,7 @@ License-File: LICENSE
|
|
|
31
31
|
Requires-Dist: requests>=2.32.3
|
|
32
32
|
Requires-Dist: typing_extensions>=4.12.2
|
|
33
33
|
Requires-Dist: jsonschema-gentypes==2.6.0
|
|
34
|
-
Requires-Dist: pydantic
|
|
34
|
+
Requires-Dist: pydantic>=2.8.0
|
|
35
35
|
Requires-Dist: stdlib_list==0.10.0
|
|
36
36
|
Requires-Dist: colorama==0.4.4
|
|
37
37
|
Requires-Dist: python-socketio[asyncio_client]==5.11.1
|
|
@@ -206,3 +206,4 @@ Please ignore \[name-defined\] errors for now. This is a known bug we are workin
|
|
|
206
206
|
## Support
|
|
207
207
|
|
|
208
208
|
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
209
|
+
.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
polyapi/__init__.py,sha256=hw7x4j9JNJfPdkIOZqV0X9pbYcw3_5AH1iQFdSogH-c,3235
|
|
2
|
+
polyapi/__main__.py,sha256=V4zhAh_YGxno5f_KSrlkELxcuDh9bR3WSd0n-2r-qQQ,93
|
|
3
|
+
polyapi/api.py,sha256=2nds6ZdNe9OHvCba4IjOPga0CAYIsib2SbhEyDDCmd8,2188
|
|
4
|
+
polyapi/auth.py,sha256=zrIGatjba5GwUTNjKj1GHQWTEDP9B-HrSzCKbLFoqvc,5336
|
|
5
|
+
polyapi/cli.py,sha256=unKqAoZ1hTGAeyYRfNQ6jO15Um7N4F95k__1qFue5bI,10659
|
|
6
|
+
polyapi/client.py,sha256=DW6ljG_xCwAo2yz23A9QfLooE6ZUDvSpdA4e_dCQjiQ,1418
|
|
7
|
+
polyapi/config.py,sha256=cAMv2n9tGN_BTvqt7V32o5F86qRhxAKyey_PoId2D8s,7638
|
|
8
|
+
polyapi/constants.py,sha256=sc-FnS0SngBLvSu1ZWMs0UCf9EYD1u1Yhfr-sZXGLns,607
|
|
9
|
+
polyapi/deployables.py,sha256=8x-Y7MlpWnTqsWhidmO6yVZe2nTChzma6hJNVsfg94s,12113
|
|
10
|
+
polyapi/error_handler.py,sha256=I_e0iz6VM23FLVQWJljxs2NGcl_OODbi43OcbnqBlp8,2398
|
|
11
|
+
polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
|
|
12
|
+
polyapi/execute.py,sha256=sjI6BMBYPSCD6UngV9DzpJIRSU6p02aShNaTXhDExtY,3457
|
|
13
|
+
polyapi/function_cli.py,sha256=H0sVrbvRBXw_xeApe2MvQw8p_xE7jVTTOU-07Dg041A,4220
|
|
14
|
+
polyapi/generate.py,sha256=slCw9AOvQHQ8UtEaumFI1NoRvjH2Dj3Y33u7imQqi8c,19521
|
|
15
|
+
polyapi/parser.py,sha256=20ZE7kSXx3UL7QVSIYYxzsnJlygVbsaDAg9q7c41WxQ,20695
|
|
16
|
+
polyapi/poly_schemas.py,sha256=760g-rBou-XT0y7N0THHHJBHlqIrOhVsAj500T6Qp0Q,8994
|
|
17
|
+
polyapi/prepare.py,sha256=pRWBhpgqMtKP04P9F6PIA3eCkOpCxQSv9TZdR3qR34I,7216
|
|
18
|
+
polyapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
polyapi/rendered_spec.py,sha256=nJEj2vRgG3N20fU4s-ThRtOIwAuTzXwXuOBIkXljDVc,2240
|
|
20
|
+
polyapi/schema.py,sha256=zMN0zr_dku-NzmL8e0C6lbym8I0SrzXxtU5I8-YwHkM,5291
|
|
21
|
+
polyapi/server.py,sha256=YXWxhYBx-hluwDQ8Jvfpy2s8ogz0GsNTMcZVNcP5ca8,2147
|
|
22
|
+
polyapi/sync.py,sha256=PGdC0feBBjEVrF3d9EluW_OAxbWuzSrfh84czma8kWg,6476
|
|
23
|
+
polyapi/typedefs.py,sha256=vJLZYBNmR3i8yQEDYlu1UfvtJyg6E1R1QyGlgFUm2rU,2362
|
|
24
|
+
polyapi/utils.py,sha256=1F7Dwst_PbPuUBUSxx5r8d2DHDgqHtu07QW92T_YSdw,12454
|
|
25
|
+
polyapi/variables.py,sha256=VAp2d5I-4WLYHCPF1w3pqU4-z8_XRQpYW-ddOw6G5S4,7268
|
|
26
|
+
polyapi/webhook.py,sha256=gWYXHz0PnB_uY_lnHeUlg3EIHfTGwF-Tc6UaatldZBw,5333
|
|
27
|
+
polyapi_python-0.3.8.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
|
|
28
|
+
polyapi_python-0.3.8.dist-info/METADATA,sha256=lGwPvkCNQgjTRTVpdJ9hjfUgpVhQ8RLJx7rVuOfLk-M,5779
|
|
29
|
+
polyapi_python-0.3.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
30
|
+
polyapi_python-0.3.8.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
|
|
31
|
+
polyapi_python-0.3.8.dist-info/RECORD,,
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
polyapi/__init__.py,sha256=hw7x4j9JNJfPdkIOZqV0X9pbYcw3_5AH1iQFdSogH-c,3235
|
|
2
|
-
polyapi/__main__.py,sha256=V4zhAh_YGxno5f_KSrlkELxcuDh9bR3WSd0n-2r-qQQ,93
|
|
3
|
-
polyapi/api.py,sha256=2nds6ZdNe9OHvCba4IjOPga0CAYIsib2SbhEyDDCmd8,2188
|
|
4
|
-
polyapi/auth.py,sha256=zrIGatjba5GwUTNjKj1GHQWTEDP9B-HrSzCKbLFoqvc,5336
|
|
5
|
-
polyapi/cli.py,sha256=jtKXARbT9AOgYTR6nf3OiwiPvsyUlLVbyynEA84PDzw,8924
|
|
6
|
-
polyapi/client.py,sha256=CoFDYvyKsqL4wPQbUDIr0Qb8Q5eD92xN4OEEcJEVuGQ,1296
|
|
7
|
-
polyapi/config.py,sha256=QQxRZ9nMUykItUMAdw97dad0DPEV1luRwkeqOyrEKf8,4316
|
|
8
|
-
polyapi/constants.py,sha256=sc-FnS0SngBLvSu1ZWMs0UCf9EYD1u1Yhfr-sZXGLns,607
|
|
9
|
-
polyapi/deployables.py,sha256=WVcNNB6W5ZW_-ukf_kK3moRcnwIkC-O4te6vLepjcco,11936
|
|
10
|
-
polyapi/error_handler.py,sha256=I_e0iz6VM23FLVQWJljxs2NGcl_OODbi43OcbnqBlp8,2398
|
|
11
|
-
polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
|
|
12
|
-
polyapi/execute.py,sha256=sjI6BMBYPSCD6UngV9DzpJIRSU6p02aShNaTXhDExtY,3457
|
|
13
|
-
polyapi/function_cli.py,sha256=hv5K5niegqitT6VwbS7M5ec3nEMyqwVtE6tcpwHoxIk,4125
|
|
14
|
-
polyapi/generate.py,sha256=1ZwcUEfhYLc64Wh5aYhvU6P594HOL2LYycPKkH39frc,15394
|
|
15
|
-
polyapi/parser.py,sha256=mdoh4pNq8pyiHE0-i6Coqj8frEXfBLRk6itpAXMrrgI,20373
|
|
16
|
-
polyapi/poly_schemas.py,sha256=T4kfZyfgVLiqLD28GmYNiHnrNx77J_HO4uzk8LUAhlo,3137
|
|
17
|
-
polyapi/prepare.py,sha256=Q8CWV4kmZ2dbXYVsud34AgJkj5ymcQ_IcYhLuikc9yk,6659
|
|
18
|
-
polyapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
polyapi/rendered_spec.py,sha256=nJEj2vRgG3N20fU4s-ThRtOIwAuTzXwXuOBIkXljDVc,2240
|
|
20
|
-
polyapi/schema.py,sha256=K5oAELfyAU3kuo1wSkTvNiA_Ppt2JCJwyg8F8XtLDU4,4707
|
|
21
|
-
polyapi/server.py,sha256=YXWxhYBx-hluwDQ8Jvfpy2s8ogz0GsNTMcZVNcP5ca8,2147
|
|
22
|
-
polyapi/sync.py,sha256=PGdC0feBBjEVrF3d9EluW_OAxbWuzSrfh84czma8kWg,6476
|
|
23
|
-
polyapi/typedefs.py,sha256=MGDwWaijLNqokXF9UCHGAP-yKixOzztrH4Lsj800AJs,2328
|
|
24
|
-
polyapi/utils.py,sha256=1F7Dwst_PbPuUBUSxx5r8d2DHDgqHtu07QW92T_YSdw,12454
|
|
25
|
-
polyapi/variables.py,sha256=j7WWrGLr2O5SkWGxnsusnnfl25kVL3b6SQYcVGEoC8c,4277
|
|
26
|
-
polyapi/webhook.py,sha256=NTSXYOl_QqsFekWRepPyVTsV9SVkgUu0HfG1SJJDHOE,4958
|
|
27
|
-
polyapi_python-0.3.7.dev7.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
|
|
28
|
-
polyapi_python-0.3.7.dev7.dist-info/METADATA,sha256=EeHNEwtPyswzeR1PoEXRKCgj0lEPA6Ak--1_pxmJjUo,5782
|
|
29
|
-
polyapi_python-0.3.7.dev7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
30
|
-
polyapi_python-0.3.7.dev7.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
|
|
31
|
-
polyapi_python-0.3.7.dev7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|