polyapi-python 0.3.8.dev6__py3-none-any.whl → 0.3.8.dev8__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 +15 -0
- polyapi/generate.py +109 -31
- polyapi/poly_schemas.py +125 -39
- polyapi/variables.py +89 -15
- polyapi/webhook.py +22 -16
- {polyapi_python-0.3.8.dev6.dist-info → polyapi_python-0.3.8.dev8.dist-info}/METADATA +1 -1
- {polyapi_python-0.3.8.dev6.dist-info → polyapi_python-0.3.8.dev8.dist-info}/RECORD +10 -10
- {polyapi_python-0.3.8.dev6.dist-info → polyapi_python-0.3.8.dev8.dist-info}/WHEEL +0 -0
- {polyapi_python-0.3.8.dev6.dist-info → polyapi_python-0.3.8.dev8.dist-info}/licenses/LICENSE +0 -0
- {polyapi_python-0.3.8.dev6.dist-info → polyapi_python-0.3.8.dev8.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
|
|
polyapi/generate.py
CHANGED
|
@@ -2,6 +2,8 @@ import json
|
|
|
2
2
|
import requests
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
|
+
import logging
|
|
6
|
+
import tempfile
|
|
5
7
|
from typing import Any, List, Optional, Tuple, cast
|
|
6
8
|
|
|
7
9
|
from .auth import render_auth_function
|
|
@@ -426,48 +428,124 @@ def add_function_file(
|
|
|
426
428
|
function_name: str,
|
|
427
429
|
spec: SpecificationDto,
|
|
428
430
|
):
|
|
429
|
-
|
|
430
|
-
|
|
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)
|
|
431
440
|
|
|
432
|
-
|
|
441
|
+
func_str, func_type_defs = render_spec(spec)
|
|
433
442
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
with open(init_path, "a") as f:
|
|
438
|
-
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")
|
|
439
446
|
|
|
440
|
-
#
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
444
488
|
|
|
445
489
|
|
|
446
490
|
def create_function(
|
|
447
491
|
spec: SpecificationDto
|
|
448
492
|
) -> None:
|
|
493
|
+
"""
|
|
494
|
+
Create a function with atomic directory and file operations.
|
|
495
|
+
|
|
496
|
+
Tracks directory creation to enable cleanup on failure.
|
|
497
|
+
"""
|
|
449
498
|
full_path = os.path.dirname(os.path.abspath(__file__))
|
|
450
499
|
folders = f"poly.{spec['context']}.{spec['name']}".split(".")
|
|
451
|
-
for
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
469
534
|
|
|
470
535
|
|
|
471
536
|
def generate_functions(functions: List[SpecificationDto]) -> None:
|
|
537
|
+
failed_functions = []
|
|
472
538
|
for func in functions:
|
|
473
|
-
|
|
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/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,33 @@ 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 = []
|
|
24
28
|
if limit_ids:
|
|
25
29
|
for spec in specs:
|
|
26
30
|
if spec["id"] in limit_ids:
|
|
27
|
-
|
|
31
|
+
try:
|
|
32
|
+
create_schema(spec)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
schema_path = f"{spec.get('context', 'unknown')}.{spec.get('name', 'unknown')}"
|
|
35
|
+
schema_id = spec.get('id', 'unknown')
|
|
36
|
+
failed_schemas.append(f"{schema_path} (id: {schema_id})")
|
|
37
|
+
logging.warning(f"WARNING: Failed to generate schema {schema_path} (id: {schema_id}): {str(e)}")
|
|
38
|
+
continue
|
|
28
39
|
else:
|
|
29
40
|
for spec in specs:
|
|
30
|
-
|
|
41
|
+
try:
|
|
42
|
+
create_schema(spec)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
schema_path = f"{spec.get('context', 'unknown')}.{spec.get('name', 'unknown')}"
|
|
45
|
+
schema_id = spec.get('id', 'unknown')
|
|
46
|
+
failed_schemas.append(f"{schema_path} (id: {schema_id})")
|
|
47
|
+
logging.warning(f"WARNING: Failed to generate schema {schema_path} (id: {schema_id}): {str(e)}")
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
if failed_schemas:
|
|
51
|
+
logging.warning(f"WARNING: {len(failed_schemas)} schema(s) failed to generate:")
|
|
52
|
+
for failed_schema in failed_schemas:
|
|
53
|
+
logging.warning(f" - {failed_schema}")
|
|
31
54
|
|
|
32
55
|
|
|
33
56
|
def add_schema_file(
|
|
@@ -35,51 +58,114 @@ def add_schema_file(
|
|
|
35
58
|
schema_name: str,
|
|
36
59
|
spec: SchemaSpecDto,
|
|
37
60
|
):
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
"""
|
|
62
|
+
Atomically add a schema file to prevent partial corruption during generation failures.
|
|
63
|
+
|
|
64
|
+
This function generates all content first, then writes files atomically using temporary files
|
|
65
|
+
to ensure that either the entire operation succeeds or no changes are made to the filesystem.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
# first lets add the import to the __init__
|
|
69
|
+
init_the_init(full_path, SCHEMA_CODE_IMPORTS)
|
|
70
|
+
|
|
71
|
+
if not spec["definition"].get("title"):
|
|
72
|
+
# very empty schemas like mews.Unit are possible
|
|
73
|
+
# add a title here to be sure they render
|
|
74
|
+
spec["definition"]["title"] = schema_name
|
|
75
|
+
|
|
76
|
+
schema_defs = render_poly_schema(spec)
|
|
77
|
+
|
|
78
|
+
if not schema_defs:
|
|
79
|
+
# If render_poly_schema failed and returned empty string, don't create any files
|
|
80
|
+
raise Exception("Schema rendering failed - empty schema content returned")
|
|
81
|
+
|
|
82
|
+
# Prepare all content first before writing any files
|
|
83
|
+
schema_namespace = to_func_namespace(schema_name)
|
|
50
84
|
init_path = os.path.join(full_path, "__init__.py")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
85
|
+
schema_file_path = os.path.join(full_path, f"_{schema_namespace}.py")
|
|
86
|
+
|
|
87
|
+
# Read current __init__.py content if it exists
|
|
88
|
+
init_content = ""
|
|
89
|
+
if os.path.exists(init_path):
|
|
90
|
+
with open(init_path, "r") as f:
|
|
91
|
+
init_content = f.read()
|
|
92
|
+
|
|
93
|
+
# Prepare new content to append to __init__.py
|
|
94
|
+
new_init_content = init_content + f"\n\nfrom ._{schema_namespace} import {schema_name}\n__all__.append('{schema_name}')\n"
|
|
95
|
+
|
|
96
|
+
# Use temporary files for atomic writes
|
|
97
|
+
# Write to __init__.py atomically
|
|
98
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_init:
|
|
99
|
+
temp_init.write(new_init_content)
|
|
100
|
+
temp_init_path = temp_init.name
|
|
101
|
+
|
|
102
|
+
# Write to schema file atomically
|
|
103
|
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, dir=full_path, suffix=".tmp") as temp_schema:
|
|
104
|
+
temp_schema.write(schema_defs)
|
|
105
|
+
temp_schema_path = temp_schema.name
|
|
106
|
+
|
|
107
|
+
# Atomic operations: move temp files to final locations
|
|
108
|
+
shutil.move(temp_init_path, init_path)
|
|
109
|
+
shutil.move(temp_schema_path, schema_file_path)
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
# Clean up any temporary files that might have been created
|
|
113
|
+
try:
|
|
114
|
+
if 'temp_init_path' in locals() and os.path.exists(temp_init_path):
|
|
115
|
+
os.unlink(temp_init_path)
|
|
116
|
+
if 'temp_schema_path' in locals() and os.path.exists(temp_schema_path):
|
|
117
|
+
os.unlink(temp_schema_path)
|
|
118
|
+
except:
|
|
119
|
+
pass # Best effort cleanup
|
|
120
|
+
|
|
121
|
+
# Re-raise the original exception
|
|
122
|
+
raise e
|
|
58
123
|
|
|
59
124
|
|
|
60
125
|
def create_schema(
|
|
61
126
|
spec: SchemaSpecDto
|
|
62
127
|
) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Create a schema with atomic directory and file operations.
|
|
130
|
+
|
|
131
|
+
Tracks directory creation to enable cleanup on failure.
|
|
132
|
+
"""
|
|
63
133
|
full_path = os.path.dirname(os.path.abspath(__file__))
|
|
64
134
|
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
|
-
|
|
135
|
+
created_dirs = [] # Track directories we create for cleanup on failure
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
for idx, folder in enumerate(folders):
|
|
139
|
+
if idx + 1 == len(folders):
|
|
140
|
+
# special handling for final level
|
|
141
|
+
add_schema_file(
|
|
142
|
+
full_path,
|
|
143
|
+
folder,
|
|
144
|
+
spec,
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
full_path = os.path.join(full_path, folder)
|
|
148
|
+
if not os.path.exists(full_path):
|
|
149
|
+
os.makedirs(full_path)
|
|
150
|
+
created_dirs.append(full_path) # Track for cleanup
|
|
151
|
+
|
|
152
|
+
# append to __init__.py file if nested folders
|
|
153
|
+
next = folders[idx + 1] if idx + 2 < len(folders) else ""
|
|
154
|
+
if next:
|
|
155
|
+
init_the_init(full_path, SCHEMA_CODE_IMPORTS)
|
|
156
|
+
add_import_to_init(full_path, next)
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
# Clean up directories we created (in reverse order)
|
|
160
|
+
for dir_path in reversed(created_dirs):
|
|
161
|
+
try:
|
|
162
|
+
if os.path.exists(dir_path) and not os.listdir(dir_path): # Only remove if empty
|
|
163
|
+
os.rmdir(dir_path)
|
|
164
|
+
except:
|
|
165
|
+
pass # Best effort cleanup
|
|
166
|
+
|
|
167
|
+
# Re-raise the original exception
|
|
168
|
+
raise e
|
|
83
169
|
|
|
84
170
|
|
|
85
171
|
def add_schema_to_init(full_path: str, spec: SchemaSpecDto):
|
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):
|
|
@@ -2,7 +2,7 @@ polyapi/__init__.py,sha256=hw7x4j9JNJfPdkIOZqV0X9pbYcw3_5AH1iQFdSogH-c,3235
|
|
|
2
2
|
polyapi/__main__.py,sha256=V4zhAh_YGxno5f_KSrlkELxcuDh9bR3WSd0n-2r-qQQ,93
|
|
3
3
|
polyapi/api.py,sha256=2nds6ZdNe9OHvCba4IjOPga0CAYIsib2SbhEyDDCmd8,2188
|
|
4
4
|
polyapi/auth.py,sha256=zrIGatjba5GwUTNjKj1GHQWTEDP9B-HrSzCKbLFoqvc,5336
|
|
5
|
-
polyapi/cli.py,sha256=
|
|
5
|
+
polyapi/cli.py,sha256=unKqAoZ1hTGAeyYRfNQ6jO15Um7N4F95k__1qFue5bI,10659
|
|
6
6
|
polyapi/client.py,sha256=DW6ljG_xCwAo2yz23A9QfLooE6ZUDvSpdA4e_dCQjiQ,1418
|
|
7
7
|
polyapi/config.py,sha256=cAMv2n9tGN_BTvqt7V32o5F86qRhxAKyey_PoId2D8s,7638
|
|
8
8
|
polyapi/constants.py,sha256=sc-FnS0SngBLvSu1ZWMs0UCf9EYD1u1Yhfr-sZXGLns,607
|
|
@@ -11,9 +11,9 @@ polyapi/error_handler.py,sha256=I_e0iz6VM23FLVQWJljxs2NGcl_OODbi43OcbnqBlp8,2398
|
|
|
11
11
|
polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
|
|
12
12
|
polyapi/execute.py,sha256=sjI6BMBYPSCD6UngV9DzpJIRSU6p02aShNaTXhDExtY,3457
|
|
13
13
|
polyapi/function_cli.py,sha256=H0sVrbvRBXw_xeApe2MvQw8p_xE7jVTTOU-07Dg041A,4220
|
|
14
|
-
polyapi/generate.py,sha256=
|
|
14
|
+
polyapi/generate.py,sha256=slCw9AOvQHQ8UtEaumFI1NoRvjH2Dj3Y33u7imQqi8c,19521
|
|
15
15
|
polyapi/parser.py,sha256=mdoh4pNq8pyiHE0-i6Coqj8frEXfBLRk6itpAXMrrgI,20373
|
|
16
|
-
polyapi/poly_schemas.py,sha256=
|
|
16
|
+
polyapi/poly_schemas.py,sha256=TC_pCuK2lGCxEruW-og4fcQzliPZEbCGzrjAOLB-kXw,7061
|
|
17
17
|
polyapi/prepare.py,sha256=pRWBhpgqMtKP04P9F6PIA3eCkOpCxQSv9TZdR3qR34I,7216
|
|
18
18
|
polyapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
polyapi/rendered_spec.py,sha256=nJEj2vRgG3N20fU4s-ThRtOIwAuTzXwXuOBIkXljDVc,2240
|
|
@@ -22,10 +22,10 @@ polyapi/server.py,sha256=YXWxhYBx-hluwDQ8Jvfpy2s8ogz0GsNTMcZVNcP5ca8,2147
|
|
|
22
22
|
polyapi/sync.py,sha256=PGdC0feBBjEVrF3d9EluW_OAxbWuzSrfh84czma8kWg,6476
|
|
23
23
|
polyapi/typedefs.py,sha256=MGDwWaijLNqokXF9UCHGAP-yKixOzztrH4Lsj800AJs,2328
|
|
24
24
|
polyapi/utils.py,sha256=1F7Dwst_PbPuUBUSxx5r8d2DHDgqHtu07QW92T_YSdw,12454
|
|
25
|
-
polyapi/variables.py,sha256=
|
|
26
|
-
polyapi/webhook.py,sha256=
|
|
27
|
-
polyapi_python-0.3.8.
|
|
28
|
-
polyapi_python-0.3.8.
|
|
29
|
-
polyapi_python-0.3.8.
|
|
30
|
-
polyapi_python-0.3.8.
|
|
31
|
-
polyapi_python-0.3.8.
|
|
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.dev8.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
|
|
28
|
+
polyapi_python-0.3.8.dev8.dist-info/METADATA,sha256=3atDYIJdS2MOsB14rghtBE4hBlCsfsaNDZHwmYIol4Q,5782
|
|
29
|
+
polyapi_python-0.3.8.dev8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
30
|
+
polyapi_python-0.3.8.dev8.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
|
|
31
|
+
polyapi_python-0.3.8.dev8.dist-info/RECORD,,
|
|
File without changes
|
{polyapi_python-0.3.8.dev6.dist-info → polyapi_python-0.3.8.dev8.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|