polyapi-python 0.3.13.dev2__tar.gz → 0.3.13.dev3__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.13.dev2/polyapi_python.egg-info → polyapi_python-0.3.13.dev3}/PKG-INFO +1 -1
  2. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/poly_tables.py +109 -41
  3. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3/polyapi_python.egg-info}/PKG-INFO +1 -1
  4. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/pyproject.toml +1 -1
  5. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_tabi.py +86 -40
  6. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/LICENSE +0 -0
  7. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/README.md +0 -0
  8. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/__init__.py +0 -0
  9. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/__main__.py +0 -0
  10. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/api.py +0 -0
  11. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/auth.py +0 -0
  12. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/cli.py +0 -0
  13. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/client.py +0 -0
  14. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/config.py +0 -0
  15. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/constants.py +0 -0
  16. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/deployables.py +0 -0
  17. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/error_handler.py +0 -0
  18. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/exceptions.py +0 -0
  19. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/execute.py +0 -0
  20. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/function_cli.py +0 -0
  21. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/generate.py +0 -0
  22. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/parser.py +0 -0
  23. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/poly_schemas.py +0 -0
  24. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/prepare.py +0 -0
  25. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/py.typed +0 -0
  26. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/rendered_spec.py +0 -0
  27. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/schema.py +0 -0
  28. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/server.py +0 -0
  29. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/sync.py +0 -0
  30. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/typedefs.py +0 -0
  31. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/utils.py +0 -0
  32. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/variables.py +0 -0
  33. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi/webhook.py +0 -0
  34. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi_python.egg-info/SOURCES.txt +0 -0
  35. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi_python.egg-info/dependency_links.txt +0 -0
  36. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi_python.egg-info/requires.txt +0 -0
  37. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/polyapi_python.egg-info/top_level.txt +0 -0
  38. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/setup.cfg +0 -0
  39. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_api.py +0 -0
  40. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_auth.py +0 -0
  41. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_deployables.py +0 -0
  42. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_generate.py +0 -0
  43. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_parser.py +0 -0
  44. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_rendered_spec.py +0 -0
  45. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_schema.py +0 -0
  46. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_server.py +0 -0
  47. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/tests/test_utils.py +0 -0
  48. {polyapi_python-0.3.13.dev2 → polyapi_python-0.3.13.dev3}/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.13.dev2
3
+ Version: 0.3.13.dev3
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
@@ -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 = ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.13.dev2
3
+ Version: 0.3.13.dev3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polyapi-python"
7
- version = "0.3.13.dev2" # bump
7
+ version = "0.3.13.dev3"
8
8
  description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers"
9
9
  authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }]
10
10
  dependencies = [
@@ -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)