polyapi-python 0.3.12.dev2__tar.gz → 0.3.13__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.12.dev2/polyapi_python.egg-info → polyapi_python-0.3.13}/PKG-INFO +9 -1
  2. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/README.md +8 -0
  3. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/deployables.py +2 -1
  4. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/parser.py +13 -3
  5. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/poly_tables.py +109 -41
  6. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/utils.py +16 -2
  7. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/webhook.py +1 -5
  8. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13/polyapi_python.egg-info}/PKG-INFO +9 -1
  9. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/pyproject.toml +4 -6
  10. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_tabi.py +86 -40
  11. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/LICENSE +0 -0
  12. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/__init__.py +0 -0
  13. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/__main__.py +0 -0
  14. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/api.py +0 -0
  15. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/auth.py +0 -0
  16. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/cli.py +0 -0
  17. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/client.py +0 -0
  18. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/config.py +0 -0
  19. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/constants.py +0 -0
  20. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/error_handler.py +0 -0
  21. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/exceptions.py +0 -0
  22. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/execute.py +0 -0
  23. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/function_cli.py +0 -0
  24. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/generate.py +0 -0
  25. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/poly_schemas.py +0 -0
  26. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/prepare.py +0 -0
  27. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/py.typed +0 -0
  28. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/rendered_spec.py +0 -0
  29. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/schema.py +0 -0
  30. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/server.py +0 -0
  31. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/sync.py +0 -0
  32. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/typedefs.py +0 -0
  33. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi/variables.py +0 -0
  34. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi_python.egg-info/SOURCES.txt +0 -0
  35. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi_python.egg-info/dependency_links.txt +0 -0
  36. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi_python.egg-info/requires.txt +0 -0
  37. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/polyapi_python.egg-info/top_level.txt +0 -0
  38. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/setup.cfg +0 -0
  39. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_api.py +0 -0
  40. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_auth.py +0 -0
  41. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_deployables.py +0 -0
  42. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_generate.py +0 -0
  43. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_parser.py +0 -0
  44. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_rendered_spec.py +0 -0
  45. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_schema.py +0 -0
  46. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_server.py +0 -0
  47. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/tests/test_utils.py +0 -0
  48. {polyapi_python-0.3.12.dev2 → polyapi_python-0.3.13}/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.12.dev2
3
+ Version: 0.3.13
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
@@ -185,6 +185,14 @@ This script is handy for checking for any mypy types:
185
185
 
186
186
  Please ignore \[name-defined\] errors for now. This is a known bug we are working to fix!
187
187
 
188
+ ## Strategies for QA'ing Changes To Generate Or Other Core Functionality
189
+
190
+ Our https://na1.polyapi.io has a large OOB catalog (as does eu1/na2). We also have several big internal PolyAPI projects with Python (message @eupharis if you need a pointer to which ones).
191
+
192
+ Running `python -m polyapi generate` in all these projects and then checking the flake8 and check_mypy steps above is a great way to build confidence that the `generate` changes has no gotchas.
193
+
194
+ Of course all this is in addition to the changes passing through normal unittests and integration tests!
195
+
188
196
  ## Support
189
197
 
190
198
  If you run into any issues or want help getting started with this project, please contact support@polyapi.io
@@ -145,6 +145,14 @@ This script is handy for checking for any mypy types:
145
145
 
146
146
  Please ignore \[name-defined\] errors for now. This is a known bug we are working to fix!
147
147
 
148
+ ## Strategies for QA'ing Changes To Generate Or Other Core Functionality
149
+
150
+ Our https://na1.polyapi.io has a large OOB catalog (as does eu1/na2). We also have several big internal PolyAPI projects with Python (message @eupharis if you need a pointer to which ones).
151
+
152
+ Running `python -m polyapi generate` in all these projects and then checking the flake8 and check_mypy steps above is a great way to build confidence that the `generate` changes has no gotchas.
153
+
154
+ Of course all this is in addition to the changes passing through normal unittests and integration tests!
155
+
148
156
  ## Support
149
157
 
150
158
  If you run into any issues or want help getting started with this project, please contact support@polyapi.io
@@ -6,6 +6,7 @@ import json
6
6
  import hashlib
7
7
  from pathlib import Path
8
8
  from typing import TypedDict, List, Dict, Tuple, Optional, Any, Union
9
+ from typing_extensions import Required
9
10
  from subprocess import check_output, CalledProcessError
10
11
 
11
12
 
@@ -54,7 +55,7 @@ class DeployableRecord(ParsedDeployableConfig, total=False):
54
55
  gitRevision: str
55
56
  fileRevision: str
56
57
  file: str
57
- types: DeployableFunctionTypes
58
+ types: Required[DeployableFunctionTypes]
58
59
  typeSchemas: Dict[str, Any]
59
60
  dependencies: List[str]
60
61
  deployments: List[Deployment]
@@ -3,7 +3,7 @@ import json
3
3
  import types
4
4
  import sys
5
5
  import re
6
- from typing import Dict, List, Mapping, Optional, Tuple, Any
6
+ from typing import Dict, List, Mapping, Optional, Tuple, Any, Union
7
7
  from typing import _TypedDictMeta as BaseTypedDict # type: ignore
8
8
  from typing_extensions import _TypedDictMeta, cast # type: ignore
9
9
  from stdlib_list import stdlib_list
@@ -390,7 +390,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
390
390
  deployable["config"] = _parse_dict(node.value)
391
391
  self._name = deployable["config"]["name"]
392
392
 
393
- def _extract_docstring_from_function(self, node: ast.FunctionDef):
393
+ def _extract_docstring_from_function(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]):
394
394
  start_lineno = (node.body[0].lineno if node.body else node.lineno) - 1
395
395
  start_offset = self._line_offsets[start_lineno]
396
396
  end_offset = start_offset
@@ -452,15 +452,19 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
452
452
  for name in node.names:
453
453
  req = _get_req_name_if_not_in_base(name.name, pip_name_lookup)
454
454
  if req:
455
+ if "dependencies" not in deployable or deployable["dependencies"] is None:
456
+ deployable["dependencies"] = []
455
457
  deployable["dependencies"].append(req)
456
458
 
457
459
  def visit_ImportFrom(self, node: ast.ImportFrom):
458
460
  if node.module:
459
461
  req = _get_req_name_if_not_in_base(node.module, pip_name_lookup)
460
462
  if req:
463
+ if "dependencies" not in deployable or deployable["dependencies"] is None:
464
+ deployable["dependencies"] = []
461
465
  deployable["dependencies"].append(req)
462
466
 
463
- def visit_FunctionDef(self, node: ast.FunctionDef):
467
+ def _handle_function_def(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]):
464
468
  if node.name == self._name:
465
469
  # Parse docstring which may contain param types and descriptions
466
470
  self._extract_docstring_from_function(node)
@@ -506,6 +510,12 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
506
510
  else:
507
511
  deployable["types"]["returns"]["type"] = "Any"
508
512
 
513
+ def visit_FunctionDef(self, node: ast.FunctionDef):
514
+ self._handle_function_def(node)
515
+
516
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
517
+ self._handle_function_def(node)
518
+
509
519
  def generic_visit(self, node):
510
520
  if hasattr(node, 'lineno') and hasattr(node, 'col_offset'):
511
521
  self._current_offset = self._line_offsets[node.lineno - 1] + node.col_offset
@@ -1,27 +1,64 @@
1
1
  import os
2
2
  import requests
3
3
  from typing_extensions import NotRequired, TypedDict
4
- from typing import List, Union, Type, Dict, Any, Literal, Tuple, Optional, get_args, get_origin
4
+ from typing import (
5
+ List,
6
+ Union,
7
+ Type,
8
+ Dict,
9
+ Any,
10
+ Literal,
11
+ Tuple,
12
+ Optional,
13
+ get_args,
14
+ get_origin,
15
+ )
5
16
  from polyapi.utils import add_import_to_init, init_the_init
6
17
  from polyapi.typedefs import TableSpecDto
7
18
  from polyapi.constants import JSONSCHEMA_TO_PYTHON_TYPE_MAP
19
+ from polyapi.config import get_api_key_and_url
8
20
 
9
- def scrub(data) -> Dict[str, Any]:
10
- if (not data or not isinstance(data, (Dict, List))): return data
21
+ TABI_MODULE_IMPORTS = "\n".join(
22
+ [
23
+ "from typing_extensions import NotRequired, TypedDict",
24
+ "from typing import Union, List, Dict, Any, Literal, Optional, Required, overload",
25
+ "from polyapi.poly_tables import execute_query, first_result, transform_query, delete_one_response",
26
+ "from polyapi.typedefs import Table, PolyCountResult, PolyDeleteResult, PolyDeleteResults, SortOrder, StringFilter, NullableStringFilter, NumberFilter, NullableNumberFilter, BooleanFilter, NullableBooleanFilter, NullableObjectFilter",
27
+ ]
28
+ )
29
+
30
+
31
+ def scrub(data: Any) -> Any:
32
+ if not data or not isinstance(data, (Dict, List)):
33
+ return data
11
34
  if isinstance(data, List):
12
35
  return [scrub(item) for item in data]
13
36
  else:
14
37
  temp = {}
15
- secrets = ["x_api_key", "x-api-key", "access_token", "access-token", "authorization", "api_key", "api-key", "apikey", "accesstoken", "token", "password", "key"]
38
+ secrets = [
39
+ "x_api_key",
40
+ "x-api-key",
41
+ "access_token",
42
+ "access-token",
43
+ "authorization",
44
+ "api_key",
45
+ "api-key",
46
+ "apikey",
47
+ "accesstoken",
48
+ "token",
49
+ "password",
50
+ "key",
51
+ ]
16
52
  for key, value in data.items():
17
53
  if isinstance(value, (Dict, List)):
18
54
  temp[key] = scrub(data[key])
19
55
  elif key.lower() in secrets:
20
- temp[key] = '********'
56
+ temp[key] = "********"
21
57
  else:
22
58
  temp[key] = data[key]
23
59
  return temp
24
60
 
61
+
25
62
  def scrub_keys(e: Exception) -> Dict[str, Any]:
26
63
  """
27
64
  Scrub the keys of an exception to remove sensitive information.
@@ -31,18 +68,26 @@ def scrub_keys(e: Exception) -> Dict[str, Any]:
31
68
  "error": str(e),
32
69
  "type": type(e).__name__,
33
70
  "message": str(e),
34
- "args": scrub(getattr(e, 'args', None))
71
+ "args": scrub(getattr(e, "args", None)),
35
72
  }
36
73
 
37
74
 
38
75
  def execute_query(table_id, method, query):
39
76
  from polyapi import polyCustom
40
77
  from polyapi.poly.client_id import client_id
78
+
41
79
  try:
42
- url = f"/tables/{table_id}/{method}?clientId={client_id}"
43
- headers = {{
44
- 'x-poly-execution-id': polyCustom.get('executionId')
45
- }}
80
+ api_key, base_url = get_api_key_and_url()
81
+ if not base_url:
82
+ raise ValueError(
83
+ "PolyAPI Instance URL is not configured, run `python -m polyapi setup`."
84
+ )
85
+
86
+ auth_key = polyCustom.get("executionApiKey") or api_key
87
+ url = f"{base_url.rstrip('/')}/tables/{table_id}/{method}?clientId={client_id}"
88
+ headers = {"x-poly-execution-id": polyCustom.get("executionId")}
89
+ if auth_key:
90
+ headers["Authorization"] = f"Bearer {auth_key}"
46
91
  response = requests.post(url, json=query, headers=headers)
47
92
  response.raise_for_status()
48
93
  return response.json()
@@ -51,14 +96,16 @@ def execute_query(table_id, method, query):
51
96
 
52
97
 
53
98
  def first_result(rsp):
54
- if isinstance(rsp, dict) and isinstance(rsp.get('results'), list):
55
- return rsp['results'][0] if rsp['results'] else None
99
+ if isinstance(rsp, dict) and isinstance(rsp.get("results"), list):
100
+ return rsp["results"][0] if rsp["results"] else None
56
101
  return rsp
57
102
 
103
+
58
104
  def delete_one_response(rsp):
59
- if isinstance(rsp, dict) and isinstance(rsp.get('deleted'), int):
60
- return { 'deleted': bool(rsp.get('deleted')) }
61
- return { 'deleted': false }
105
+ if isinstance(rsp, dict) and isinstance(rsp.get("deleted"), int):
106
+ return {"deleted": bool(rsp.get("deleted"))}
107
+ return {"deleted": False}
108
+
62
109
 
63
110
  _key_transform_map = {
64
111
  "not_": "not",
@@ -72,8 +119,7 @@ _key_transform_map = {
72
119
  def _transform_keys(obj: Any) -> Any:
73
120
  if isinstance(obj, dict):
74
121
  return {
75
- _key_transform_map.get(k, k): _transform_keys(v)
76
- for k, v in obj.items()
122
+ _key_transform_map.get(k, k): _transform_keys(v) for k, v in obj.items()
77
123
  }
78
124
 
79
125
  elif isinstance(obj, list):
@@ -88,13 +134,13 @@ def transform_query(query: dict) -> dict:
88
134
  return {
89
135
  **query,
90
136
  "where": _transform_keys(query["where"]) if query["where"] else None,
91
- "orderBy": query["order_by"] if query["order_by"] else None
137
+ "orderBy": query["order_by"] if query["order_by"] else None,
92
138
  }
93
139
 
94
140
  return query
95
141
 
96
142
 
97
- TABI_TABLE_TEMPLATE = '''
143
+ TABI_TABLE_TEMPLATE = """
98
144
  {table_name}Columns = Literal[{table_columns}]
99
145
 
100
146
 
@@ -369,7 +415,7 @@ class {table_name}:{table_description}
369
415
  query["where"]["id"] = kwargs["id"]
370
416
  query.pop("id", None)
371
417
  return delete_one_response(execute_query({table_name}.table_id, "delete", transform_query(query)))
372
- '''
418
+ """
373
419
 
374
420
 
375
421
  def _get_column_type_str(name: str, schema: Dict[str, Any], is_required: bool) -> str:
@@ -377,11 +423,17 @@ def _get_column_type_str(name: str, schema: Dict[str, Any], is_required: bool) -
377
423
 
378
424
  col_type = schema.get("type", "object")
379
425
  if isinstance(col_type, list):
380
- subtypes = [_get_column_type_str(name, { **schema, "type": t }, is_required) for t in col_type]
426
+ subtypes = [
427
+ _get_column_type_str(name, {**schema, "type": t}, is_required)
428
+ for t in col_type
429
+ ]
381
430
  result = f"Union[{', '.join(subtypes)}]"
382
431
  elif col_type == "array":
383
432
  if isinstance(schema["items"], list):
384
- subtypes = [_get_column_type_str(f"{name}{i}", s, True) for i, s in enumerate(schema["items"])]
433
+ subtypes = [
434
+ _get_column_type_str(f"{name}{i}", s, True)
435
+ for i, s in enumerate(schema["items"])
436
+ ]
385
437
  result = f"Tuple[{', '.join(subtypes)}]"
386
438
  elif isinstance(schema["items"], dict):
387
439
  result = f"List[{_get_column_type_str(name, schema['items'], True)}]"
@@ -391,7 +443,10 @@ def _get_column_type_str(name: str, schema: Dict[str, Any], is_required: bool) -
391
443
  if isinstance(schema.get("patternProperties"), dict):
392
444
  # TODO: Handle multiple pattern properties
393
445
  result = f"Dict[str, {_get_column_type_str(f'{name}_', schema['patternProperties'], True)}]"
394
- elif isinstance(schema.get("properties"), dict) and len(schema["properties"].values()) > 0:
446
+ elif (
447
+ isinstance(schema.get("properties"), dict)
448
+ and len(schema["properties"].values()) > 0
449
+ ):
395
450
  # TODO: Handle x-poly-refs
396
451
  result = f'"{name}"'
397
452
  else:
@@ -413,24 +468,32 @@ def _render_table_row_classes(table_name: str, schema: Dict[str, Any]) -> str:
413
468
  return output[1].split("\n", 1)[1].strip()
414
469
 
415
470
 
416
- def _render_table_subset_class(table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]) -> str:
471
+ def _render_table_subset_class(
472
+ table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]
473
+ ) -> str:
417
474
  # Generate class which can match any subset of a table row
418
475
  lines = [f"class {table_name}Subset(TypedDict):"]
419
476
 
420
477
  for name, schema in columns:
421
- type_str = _get_column_type_str(f"_{table_name}Row{name}", schema, name in required)
478
+ type_str = _get_column_type_str(
479
+ f"_{table_name}Row{name}", schema, name in required
480
+ )
422
481
  lines.append(f" {name}: NotRequired[{type_str}]")
423
482
 
424
483
  return "\n".join(lines)
425
484
 
426
485
 
427
- def _render_table_where_class(table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]) -> str:
486
+ def _render_table_where_class(
487
+ table_name: str, columns: List[Tuple[str, Dict[str, Any]]], required: List[str]
488
+ ) -> str:
428
489
  # Generate class for the 'where' part of the query
429
490
  lines = [f"class {table_name}WhereFilter(TypedDict):"]
430
491
 
431
492
  for name, schema in columns:
432
493
  ftype_str = ""
433
- type_str = _get_column_type_str(f"_{table_name}Row{name}", schema, True) # force required to avoid wrapping type in Optional[]
494
+ type_str = _get_column_type_str(
495
+ f"_{table_name}Row{name}", schema, True
496
+ ) # force required to avoid wrapping type in Optional[]
434
497
  is_required = name in required
435
498
  if type_str == "bool":
436
499
  ftype_str = "BooleanFilter" if is_required else "NullableBooleanFilter"
@@ -445,24 +508,34 @@ def _render_table_where_class(table_name: str, columns: List[Tuple[str, Dict[str
445
508
  if ftype_str:
446
509
  lines.append(f" {name}: NotRequired[Union[{type_str}, {ftype_str}]]")
447
510
 
448
- lines.append(f' AND: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]')
511
+ lines.append(
512
+ f' AND: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]'
513
+ )
449
514
  lines.append(f' OR: NotRequired[List["{table_name}WhereFilter"]]')
450
- lines.append(f' NOT: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]')
515
+ lines.append(
516
+ f' NOT: NotRequired[Union["{table_name}WhereFilter", List["{table_name}WhereFilter"]]]'
517
+ )
451
518
 
452
519
  return "\n".join(lines)
453
520
 
454
521
 
455
522
  def _render_table(table: TableSpecDto) -> str:
456
523
  columns = list(table["schema"]["properties"].items())
457
- required_colunms = table["schema"].get("required", [])
524
+ required_columns = table["schema"].get("required", [])
458
525
 
459
- table_columns = ",".join([ f'"{k}"' for k,_ in columns])
526
+ table_columns = ",".join([f'"{k}"' for k, _ in columns])
460
527
  table_row_classes = _render_table_row_classes(table["name"], table["schema"])
461
- table_row_subset_class = _render_table_subset_class(table["name"], columns, required_colunms)
462
- table_where_class = _render_table_where_class(table["name"], columns, required_colunms)
528
+ table_row_subset_class = _render_table_subset_class(
529
+ table["name"], columns, required_columns
530
+ )
531
+ table_where_class = _render_table_where_class(
532
+ table["name"], columns, required_columns
533
+ )
463
534
  if table.get("description", ""):
464
- table_description = '\n """'
465
- table_description += '\n '.join(table["description"].replace('"', "'").split("\n"))
535
+ table_description = '\n """'
536
+ table_description += "\n ".join(
537
+ table["description"].replace('"', "'").split("\n")
538
+ )
466
539
  table_description += '\n """'
467
540
  else:
468
541
  table_description = ""
@@ -502,12 +575,7 @@ def _create_table(table: TableSpecDto) -> None:
502
575
 
503
576
  init_path = os.path.join(full_path, "__init__.py")
504
577
 
505
- imports = "\n".join([
506
- "from typing_extensions import NotRequired, TypedDict",
507
- "from typing import Union, List, Dict, Any, Literal, Optional, Required, overload",
508
- "from polyapi.poly_tables import execute_query, first_result, transform_query",
509
- "from polyapi.typedefs import Table, PolyCountResult, PolyDeleteResults, SortOrder, StringFilter, NullableStringFilter, NumberFilter, NullableNumberFilter, BooleanFilter, NullableBooleanFilter, NullableObjectFilter",
510
- ])
578
+ imports = TABI_MODULE_IMPORTS
511
579
  table_contents = _render_table(table)
512
580
 
513
581
  file_contents = ""
@@ -73,9 +73,23 @@ def print_red(s: str):
73
73
 
74
74
  def add_type_import_path(function_name: str, arg: str) -> str:
75
75
  """if not basic type, coerce to camelCase and add the import path"""
76
- # for now, just treat Callables as basic types
76
+ # outdated og comment - for now, just treat Callables as basic types
77
+ # from now, we start qualifying non-basic types :))
78
+ # e.g. Callable[[EmailAddress, Dict, Dict, Dict], None]
79
+ # becomes Callable[[Set_profile_email.EmailAddress, Dict, Dict, Dict], None]
80
+
77
81
  if arg.startswith("Callable"):
78
- return arg
82
+ inner = arg[len("Callable["):-1] # strip outer Callable[...]
83
+ parts = [p.strip() for p in inner.split(",")]
84
+ qualified = []
85
+ for p in parts:
86
+ clean = p.strip("[] ")
87
+ if clean and clean not in BASIC_PYTHON_TYPES:
88
+ replacement = f"{to_func_namespace(function_name)}.{camelCase(clean)}"
89
+ p = p.replace(clean, replacement)
90
+ qualified.append(p)
91
+ return "Callable[" + ",".join(qualified) + "]"
92
+ # return arg
79
93
 
80
94
  if arg in BASIC_PYTHON_TYPES:
81
95
  return arg
@@ -124,11 +124,7 @@ def render_webhook_handle(
124
124
  ) -> Tuple[str, str]:
125
125
  try:
126
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
-
127
+
132
128
  func_str = WEBHOOK_TEMPLATE.format(
133
129
  description=function_description,
134
130
  function_id=function_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.12.dev2
3
+ Version: 0.3.13
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
@@ -185,6 +185,14 @@ This script is handy for checking for any mypy types:
185
185
 
186
186
  Please ignore \[name-defined\] errors for now. This is a known bug we are working to fix!
187
187
 
188
+ ## Strategies for QA'ing Changes To Generate Or Other Core Functionality
189
+
190
+ Our https://na1.polyapi.io has a large OOB catalog (as does eu1/na2). We also have several big internal PolyAPI projects with Python (message @eupharis if you need a pointer to which ones).
191
+
192
+ Running `python -m polyapi generate` in all these projects and then checking the flake8 and check_mypy steps above is a great way to build confidence that the `generate` changes has no gotchas.
193
+
194
+ Of course all this is in addition to the changes passing through normal unittests and integration tests!
195
+
188
196
  ## Support
189
197
 
190
198
  If you run into any issues or want help getting started with this project, please contact support@polyapi.io
@@ -1,9 +1,10 @@
1
1
  [build-system]
2
2
  requires = ["setuptools>=61.2", "wheel"]
3
+ build-backend = "setuptools.build_meta"
3
4
 
4
5
  [project]
5
6
  name = "polyapi-python"
6
- version = "0.3.12.dev2"
7
+ version = "0.3.13"
7
8
  description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers"
8
9
  authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }]
9
10
  dependencies = [
@@ -23,14 +24,11 @@ requires-python = ">=3.10"
23
24
  [project.urls]
24
25
  Homepage = "https://github.com/polyapi/polyapi-python"
25
26
 
26
- [tool.setuptools]
27
- packages = ["polyapi"]
28
-
29
- [tools.setuptools.packages.find]
27
+ [tool.setuptools.packages.find]
30
28
  include = ["polyapi"]
31
29
  exclude = ["polyapi/poly*", "polyapi/vari*", "polyapi/.config.env", "polyapi/cached_deployables*", "polyapi/deployments_revision"] # exclude the generated libraries from builds
32
30
 
33
31
  [tool.mypy]
34
32
  # for now redef errors happen sometimes, we will clean this up in the future!
35
33
  disable_error_code = "no-redef,name-defined"
36
- implicit_optional = true
34
+ implicit_optional = true
@@ -1,8 +1,16 @@
1
1
  import unittest
2
- from polyapi.poly_tables import _render_table
2
+ from unittest.mock import Mock, patch
3
+ from polyapi.poly_tables import _render_table, TABI_MODULE_IMPORTS, execute_query
4
+ from polyapi.typedefs import TableSpecDto
3
5
 
4
6
 
5
- TABLE_SPEC_SIMPLE = {
7
+ def _normalize_type_notation(value: str) -> str:
8
+ # Python/runtime/tooling versions may emit either built-in generic style
9
+ # (dict[str, Any]) or typing style (Dict[str, Any]) for the same schema.
10
+ return value.replace("dict[str, Any]", "Dict[str, Any]")
11
+
12
+
13
+ TABLE_SPEC_SIMPLE: TableSpecDto = {
6
14
  "type": "table",
7
15
  "id": "123456789",
8
16
  "name": "MyTable",
@@ -13,24 +21,18 @@ TABLE_SPEC_SIMPLE = {
13
21
  "$schema": "http://json-schema.org/draft-06/schema#",
14
22
  "type": "object",
15
23
  "properties": {
16
- "id": { "type": "string" },
17
- "createdAt": { "type": "string" },
18
- "updatedAt": { "type": "string" },
19
- "name": { "type": "string" },
20
- "age": { "type": "integer" },
21
- "active": { "type": "boolean" },
22
- "optional": { "type": "object" }
24
+ "id": {"type": "string"},
25
+ "createdAt": {"type": "string"},
26
+ "updatedAt": {"type": "string"},
27
+ "name": {"type": "string"},
28
+ "age": {"type": "integer"},
29
+ "active": {"type": "boolean"},
30
+ "optional": {"type": "object"},
23
31
  },
24
- "required": [
25
- "id",
26
- "createdAt",
27
- "updatedAt",
28
- "name",
29
- "age",
30
- "active"
31
- ],
32
+ "required": ["id", "createdAt", "updatedAt", "name", "age", "active"],
32
33
  "additionalProperties": False,
33
- }
34
+ },
35
+ "unresolvedPolySchemaRefs": [],
34
36
  }
35
37
 
36
38
  EXPECTED_SIMPLE = '''
@@ -57,7 +59,7 @@ class MyTableRow(TypedDict, total=False):
57
59
  active: Required[bool]
58
60
  """ Required property """
59
61
 
60
- optional: dict[str, Any]
62
+ optional: Dict[str, Any]
61
63
 
62
64
 
63
65
 
@@ -351,43 +353,40 @@ class MyTable:
351
353
  return delete_one_response(execute_query(MyTable.table_id, "delete", transform_query(query)))
352
354
  '''
353
355
 
354
- TABLE_SPEC_COMPLEX = {
356
+ TABLE_SPEC_COMPLEX: TableSpecDto = {
355
357
  "type": "table",
356
358
  "id": "123456789",
357
359
  "name": "MyTable",
358
360
  "context": "some.context.here",
359
361
  "contextName": "some.context.here.MyTable",
362
+ "description": "",
360
363
  "schema": {
361
364
  "$schema": "http://json-schema.org/draft-06/schema#",
362
365
  "type": "object",
363
366
  "properties": {
364
- "id": { "type": "string" },
365
- "createdAt": { "type": "string" },
366
- "updatedAt": { "type": "string" },
367
+ "id": {"type": "string"},
368
+ "createdAt": {"type": "string"},
369
+ "updatedAt": {"type": "string"},
367
370
  "data": {
368
371
  "type": "object",
369
372
  "properties": {
370
- "foo": { "type": "string" },
373
+ "foo": {"type": "string"},
371
374
  "nested": {
372
375
  "type": "array",
373
376
  "items": {
374
377
  "type": "object",
375
- "properties": { "name": { "type": "string" } },
376
- "required": ["name"]
377
- }
378
+ "properties": {"name": {"type": "string"}},
379
+ "required": ["name"],
380
+ },
378
381
  },
379
- "other": { "x-poly-ref": { "path": "some.other.Schema" }}
380
- }
381
- }
382
+ "other": {"x-poly-ref": {"path": "some.other.Schema"}},
383
+ },
384
+ },
382
385
  },
383
- "required": [
384
- "id",
385
- "createdAt",
386
- "updatedAt",
387
- "data"
388
- ],
386
+ "required": ["id", "createdAt", "updatedAt", "data"],
389
387
  "additionalProperties": False,
390
- }
388
+ },
389
+ "unresolvedPolySchemaRefs": [],
391
390
  }
392
391
 
393
392
  EXPECTED_COMPLEX = '''
@@ -657,14 +656,61 @@ class MyTable:
657
656
  return execute_query(MyTable.table_id, "delete", query)
658
657
  '''
659
658
 
659
+
660
660
  class T(unittest.TestCase):
661
661
  def test_render_simple(self):
662
662
  self.maxDiff = 20000
663
663
  output = _render_table(TABLE_SPEC_SIMPLE)
664
- self.assertEqual(output, EXPECTED_SIMPLE)
665
-
664
+ self.assertEqual(
665
+ _normalize_type_notation(output),
666
+ _normalize_type_notation(EXPECTED_SIMPLE),
667
+ )
668
+
669
+ def test_execute_query_does_not_return_unhashable_dict_error(self):
670
+ result = execute_query("test-table", "select", {})
671
+ self.assertIsInstance(result, dict)
672
+ self.assertNotIn("unhashable type: 'dict'", str(result))
673
+
674
+ def test_execute_query_uses_absolute_url_and_auth_header(self):
675
+ response = Mock()
676
+ response.raise_for_status.return_value = None
677
+ response.json.return_value = {"ok": True}
678
+
679
+ with (
680
+ patch(
681
+ "polyapi.poly_tables.get_api_key_and_url",
682
+ return_value=("test-api-key", "https://na1.polyapi.io"),
683
+ ),
684
+ patch(
685
+ "polyapi.poly_tables.requests.post", return_value=response
686
+ ) as post_mock,
687
+ ):
688
+ result = execute_query("table-id-123", "select", {"where": {"id": "abc"}})
689
+
690
+ self.assertEqual(result, {"ok": True})
691
+ post_mock.assert_called_once()
692
+ called_url = (
693
+ post_mock.call_args.kwargs["url"]
694
+ if "url" in post_mock.call_args.kwargs
695
+ else post_mock.call_args.args[0]
696
+ )
697
+ called_headers = post_mock.call_args.kwargs["headers"]
698
+ self.assertEqual(
699
+ called_url.split("?")[0],
700
+ "https://na1.polyapi.io/tables/table-id-123/select",
701
+ )
702
+ self.assertEqual(called_headers["Authorization"], "Bearer test-api-key")
703
+
704
+ def test_generated_module_executes_with_delete_one_types(self):
705
+ source = f"{TABI_MODULE_IMPORTS}\n\n\n{_render_table(TABLE_SPEC_SIMPLE)}"
706
+ generated_scope = {}
707
+ exec(source, generated_scope)
708
+ self.assertIn("MyTable", generated_scope)
709
+ self.assertIn("PolyDeleteResult", generated_scope)
710
+ self.assertIn("delete_one_response", generated_scope)
711
+
666
712
  @unittest.skip("too brittle, will restore later")
667
713
  def test_render_complex(self):
668
714
  self.maxDiff = 20000
669
715
  output = _render_table(TABLE_SPEC_COMPLEX)
670
- self.assertEqual(output, EXPECTED_COMPLEX)
716
+ self.assertEqual(output, EXPECTED_COMPLEX)