polyapi-python 0.3.8.dev7__tar.gz → 0.3.8.dev8__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.
Files changed (48) hide show
  1. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/PKG-INFO +1 -1
  2. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/generate.py +109 -31
  3. polyapi_python-0.3.8.dev8/polyapi/poly_schemas.py +186 -0
  4. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/variables.py +89 -15
  5. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/webhook.py +22 -16
  6. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi_python.egg-info/PKG-INFO +1 -1
  7. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/pyproject.toml +1 -1
  8. polyapi_python-0.3.8.dev8/tests/test_generate.py +662 -0
  9. polyapi_python-0.3.8.dev7/polyapi/poly_schemas.py +0 -100
  10. polyapi_python-0.3.8.dev7/tests/test_generate.py +0 -289
  11. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/LICENSE +0 -0
  12. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/README.md +0 -0
  13. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/__init__.py +0 -0
  14. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/__main__.py +0 -0
  15. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/api.py +0 -0
  16. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/auth.py +0 -0
  17. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/cli.py +0 -0
  18. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/client.py +0 -0
  19. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/config.py +0 -0
  20. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/constants.py +0 -0
  21. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/deployables.py +0 -0
  22. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/error_handler.py +0 -0
  23. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/exceptions.py +0 -0
  24. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/execute.py +0 -0
  25. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/function_cli.py +0 -0
  26. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/parser.py +0 -0
  27. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/prepare.py +0 -0
  28. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/py.typed +0 -0
  29. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/rendered_spec.py +0 -0
  30. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/schema.py +0 -0
  31. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/server.py +0 -0
  32. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/sync.py +0 -0
  33. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/typedefs.py +0 -0
  34. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi/utils.py +0 -0
  35. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi_python.egg-info/SOURCES.txt +0 -0
  36. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi_python.egg-info/dependency_links.txt +0 -0
  37. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi_python.egg-info/requires.txt +0 -0
  38. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/polyapi_python.egg-info/top_level.txt +0 -0
  39. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/setup.cfg +0 -0
  40. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/tests/test_api.py +0 -0
  41. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/tests/test_auth.py +0 -0
  42. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/tests/test_deployables.py +0 -0
  43. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/tests/test_parser.py +0 -0
  44. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/tests/test_rendered_spec.py +0 -0
  45. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/tests/test_schema.py +0 -0
  46. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/tests/test_server.py +0 -0
  47. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/tests/test_utils.py +0 -0
  48. {polyapi_python-0.3.8.dev7 → polyapi_python-0.3.8.dev8}/tests/test_variables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.8.dev7
3
+ Version: 0.3.8.dev8
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
@@ -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
- # first lets add the import to the __init__
430
- init_the_init(full_path)
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
- func_str, func_type_defs = render_spec(spec)
441
+ func_str, func_type_defs = render_spec(spec)
433
442
 
434
- if func_str:
435
- # add function to init
436
- init_path = os.path.join(full_path, "__init__.py")
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
- # add type_defs to underscore file
441
- file_path = os.path.join(full_path, f"{to_func_namespace(function_name)}.py")
442
- with open(file_path, "w") as f:
443
- f.write(func_type_defs)
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 idx, folder in enumerate(folders):
452
- if idx + 1 == len(folders):
453
- # special handling for final level
454
- add_function_file(
455
- full_path,
456
- folder,
457
- spec,
458
- )
459
- else:
460
- full_path = os.path.join(full_path, folder)
461
- if not os.path.exists(full_path):
462
- os.makedirs(full_path)
463
-
464
- # append to __init__.py file if nested folders
465
- next = folders[idx + 1] if idx + 2 < len(folders) else ""
466
- if next:
467
- init_the_init(full_path)
468
- add_import_to_init(full_path, next)
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
- create_function(func)
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}")
@@ -0,0 +1,186 @@
1
+ import os
2
+ import logging
3
+ import tempfile
4
+ import shutil
5
+ from typing import Any, Dict, List, Tuple
6
+
7
+ from polyapi.schema import wrapped_generate_schema_types
8
+ from polyapi.utils import add_import_to_init, init_the_init, to_func_namespace
9
+
10
+ from .typedefs import SchemaSpecDto
11
+
12
+ SCHEMA_CODE_IMPORTS = """from typing_extensions import TypedDict, NotRequired
13
+
14
+ __all__ = []
15
+
16
+
17
+ """
18
+
19
+
20
+ FALLBACK_SPEC_TEMPLATE = """class {name}(TypedDict, total=False):
21
+ ''' unable to generate schema for {name}, defaulting to permissive type '''
22
+ pass
23
+ """
24
+
25
+
26
+ def generate_schemas(specs: List[SchemaSpecDto], limit_ids: List[str] = None):
27
+ failed_schemas = []
28
+ if limit_ids:
29
+ for spec in specs:
30
+ if spec["id"] in limit_ids:
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
39
+ else:
40
+ for spec in specs:
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}")
54
+
55
+
56
+ def add_schema_file(
57
+ full_path: str,
58
+ schema_name: str,
59
+ spec: SchemaSpecDto,
60
+ ):
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)
84
+ init_path = os.path.join(full_path, "__init__.py")
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
123
+
124
+
125
+ def create_schema(
126
+ spec: SchemaSpecDto
127
+ ) -> None:
128
+ """
129
+ Create a schema with atomic directory and file operations.
130
+
131
+ Tracks directory creation to enable cleanup on failure.
132
+ """
133
+ full_path = os.path.dirname(os.path.abspath(__file__))
134
+ folders = f"schemas.{spec['context']}.{spec['name']}".split(".")
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
169
+
170
+
171
+ def add_schema_to_init(full_path: str, spec: SchemaSpecDto):
172
+ init_the_init(full_path, code_imports="")
173
+ init_path = os.path.join(full_path, "__init__.py")
174
+ with open(init_path, "a") as f:
175
+ f.write(render_poly_schema(spec) + "\n\n")
176
+
177
+
178
+ def render_poly_schema(spec: SchemaSpecDto) -> str:
179
+ definition = spec["definition"]
180
+ if not definition.get("type"):
181
+ definition["type"] = "object"
182
+ root, schema_types = wrapped_generate_schema_types(
183
+ definition, root=spec["name"], fallback_type=Dict
184
+ )
185
+ return schema_types
186
+ # return FALLBACK_SPEC_TEMPLATE.format(name=spec["name"])
@@ -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
- create_variable(variable)
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
- for idx, folder in enumerate(folders):
127
- full_path = os.path.join(full_path, folder)
128
- if not os.path.exists(full_path):
129
- os.makedirs(full_path)
130
- next = folders[idx + 1] if idx + 1 < len(folders) else None
131
- if next:
132
- add_import_to_init(full_path, next)
133
-
134
- add_variable_to_init(full_path, variable)
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
- init_the_init(full_path)
139
- init_path = os.path.join(full_path, "__init__.py")
140
- with open(init_path, "a") as f:
141
- f.write(render_variable(variable) + "\n\n")
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
@@ -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
- function_args, function_args_def = parse_arguments(function_name, arguments)
125
-
126
- if "WebhookEventType" in function_args:
127
- # let's add the function name import!
128
- function_args = function_args.replace("WebhookEventType", f"{to_func_namespace(function_name)}.WebhookEventType")
129
-
130
- func_str = WEBHOOK_TEMPLATE.format(
131
- description=function_description,
132
- client_id=uuid.uuid4().hex,
133
- function_id=function_id,
134
- function_name=function_name,
135
- function_args=function_args,
136
- function_path=poly_full_path(function_context, function_name),
137
- )
138
- func_defs = WEBHOOK_DEFS_TEMPLATE.format(function_args_def=function_args_def)
139
- return func_str, func_defs
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.8.dev7
3
+ Version: 0.3.8.dev8
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
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.2", "wheel"]
3
3
 
4
4
  [project]
5
5
  name = "polyapi-python"
6
- version = "0.3.8.dev7"
6
+ version = "0.3.8.dev8"
7
7
  description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers"
8
8
  authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }]
9
9
  dependencies = [