codecraftai-restclient-twingate 1.2.0__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.
Files changed (54) hide show
  1. ccai/restclient/twingate/__init__.py +34 -0
  2. ccai/restclient/twingate/_codegen/__init__.py +12 -0
  3. ccai/restclient/twingate/_codegen/emitter.py +189 -0
  4. ccai/restclient/twingate/_codegen/import_schema.py +182 -0
  5. ccai/restclient/twingate/_codegen/typemap.py +76 -0
  6. ccai/restclient/twingate/_spec/README.md +32 -0
  7. ccai/restclient/twingate/_spec/schema.json +230 -0
  8. ccai/restclient/twingate/_spec/schema.sha256 +1 -0
  9. ccai/restclient/twingate/acltranslator/__init__.py +37 -0
  10. ccai/restclient/twingate/acltranslator/differ.py +154 -0
  11. ccai/restclient/twingate/acltranslator/executor.py +172 -0
  12. ccai/restclient/twingate/acltranslator/intermediate.py +76 -0
  13. ccai/restclient/twingate/acltranslator/parser.py +305 -0
  14. ccai/restclient/twingate/acltranslator/plan.py +80 -0
  15. ccai/restclient/twingate/apigroups/__init__.py +29 -0
  16. ccai/restclient/twingate/apigroups/connectorsgroup.py +367 -0
  17. ccai/restclient/twingate/apigroups/devicesgroup.py +248 -0
  18. ccai/restclient/twingate/apigroups/graphqlsupport.py +171 -0
  19. ccai/restclient/twingate/apigroups/groupsgroup.py +254 -0
  20. ccai/restclient/twingate/apigroups/remotenetworksgroup.py +266 -0
  21. ccai/restclient/twingate/apigroups/resourcesgroup.py +373 -0
  22. ccai/restclient/twingate/aspectstwingate.py +115 -0
  23. ccai/restclient/twingate/exceptions.py +229 -0
  24. ccai/restclient/twingate/graphqlrequestbody.py +61 -0
  25. ccai/restclient/twingate/inputs/__init__.py +22 -0
  26. ccai/restclient/twingate/inputs/resourceprotocolsinput.py +42 -0
  27. ccai/restclient/twingate/model/__init__.py +34 -0
  28. ccai/restclient/twingate/model/connectorstate.py +34 -0
  29. ccai/restclient/twingate/model/twingateconnector.py +40 -0
  30. ccai/restclient/twingate/model/twingatedevice.py +50 -0
  31. ccai/restclient/twingate/model/twingategroup.py +38 -0
  32. ccai/restclient/twingate/model/twingateremotenetwork.py +38 -0
  33. ccai/restclient/twingate/model/twingateresource.py +47 -0
  34. ccai/restclient/twingate/mutations/__init__.py +62 -0
  35. ccai/restclient/twingate/mutations/build_connector_mutations.py +109 -0
  36. ccai/restclient/twingate/mutations/build_device_mutations.py +46 -0
  37. ccai/restclient/twingate/mutations/build_group_mutations.py +102 -0
  38. ccai/restclient/twingate/mutations/build_remotenetwork_mutations.py +114 -0
  39. ccai/restclient/twingate/mutations/build_resource_mutations.py +119 -0
  40. ccai/restclient/twingate/mutations/build_resourceaccess_mutations.py +88 -0
  41. ccai/restclient/twingate/py.typed +0 -0
  42. ccai/restclient/twingate/queries/__init__.py +44 -0
  43. ccai/restclient/twingate/queries/build_connectors.py +56 -0
  44. ccai/restclient/twingate/queries/build_devices.py +55 -0
  45. ccai/restclient/twingate/queries/build_groups.py +55 -0
  46. ccai/restclient/twingate/queries/build_node.py +145 -0
  47. ccai/restclient/twingate/queries/build_remotenetworks.py +56 -0
  48. ccai/restclient/twingate/queries/build_resources.py +57 -0
  49. ccai/restclient/twingate/queries/buildersupport.py +48 -0
  50. ccai/restclient/twingate/twingateclient.py +630 -0
  51. codecraftai_restclient_twingate-1.2.0.dist-info/METADATA +100 -0
  52. codecraftai_restclient_twingate-1.2.0.dist-info/RECORD +54 -0
  53. codecraftai_restclient_twingate-1.2.0.dist-info/WHEEL +4 -0
  54. codecraftai_restclient_twingate-1.2.0.dist-info/licenses/LICENSE.md +204 -0
@@ -0,0 +1,34 @@
1
+ """
2
+ .. module:: twingate
3
+ :platform: Darwin, Linux, Unix, Windows
4
+ :synopsis: The ``codecraftai-restclient-twingate`` package — a typed,
5
+ aspect-driven GraphQL client for the Twingate Admin API.
6
+
7
+ The package re-exports :class:`TwingateClient` (which implements the
8
+ :class:`IVPNControlPlane` protocol), the :class:`AspectsTwingate`
9
+ behaviour bundle and the :class:`GraphQLRequestBody` request dataclass.
10
+
11
+ .. moduleauthor:: Myron Walker <myron.walker@gmail.com>
12
+ """
13
+
14
+ __author__ = "Myron Walker"
15
+ __copyright__ = "Copyright 2025, Code Craft AI LLC"
16
+ __credits__ = []
17
+
18
+
19
+ from ccai.restclient.twingate.twingateclient import (
20
+ TwingateClient, GRAPHQL_LEAF
21
+ )
22
+ from ccai.restclient.twingate.aspectstwingate import (
23
+ AspectsTwingate, DEFAULT_TWINGATE_ASPECTS
24
+ )
25
+ from ccai.restclient.twingate.graphqlrequestbody import GraphQLRequestBody
26
+
27
+
28
+ __all__ = [
29
+ "TwingateClient",
30
+ "GRAPHQL_LEAF",
31
+ "AspectsTwingate",
32
+ "DEFAULT_TWINGATE_ASPECTS",
33
+ "GraphQLRequestBody",
34
+ ]
@@ -0,0 +1,12 @@
1
+ """
2
+ .. module:: _codegen
3
+ :platform: Darwin, Linux, Unix, Windows
4
+ :synopsis: Maintainer-facing GraphQL schema introspection and emitter
5
+ package. See ``import_schema.py`` for the regeneration flow.
6
+
7
+ .. moduleauthor:: Myron Walker <myron.walker@gmail.com>
8
+ """
9
+
10
+ __author__ = "Myron Walker"
11
+ __copyright__ = "Copyright 2025, Code Craft AI LLC"
12
+ __credits__ = []
@@ -0,0 +1,189 @@
1
+ """
2
+ .. module:: emitter
3
+ :platform: Darwin, Linux, Unix, Windows
4
+ :synopsis: Module that contains the schema emitter — walks a GraphQL
5
+ introspection ``__schema`` payload and produces Python
6
+ source text for ``SchemaObject`` and ``enum.Enum`` types.
7
+
8
+ See the "Codegen Decision" section of ``AGENTS.md``. The emitter is
9
+ retained so a maintainer with a live network can regenerate; the
10
+ checked-in ``model/`` / ``inputs/`` modules were hand-authored from the
11
+ documented Twingate surface because no live network was available at
12
+ build time.
13
+
14
+ .. moduleauthor:: Myron Walker <myron.walker@gmail.com>
15
+ """
16
+
17
+ __author__ = "Myron Walker"
18
+ __copyright__ = "Copyright 2025, Code Craft AI LLC"
19
+ __credits__ = []
20
+
21
+
22
+ from typing import Any, Dict, List
23
+
24
+ from ccai.restclient.twingate._codegen.typemap import (
25
+ python_typename_for_scalar
26
+ )
27
+
28
+
29
+ GENERATED_HEADER = (
30
+ '"""\n'
31
+ " DO NOT EDIT — generated by\n"
32
+ " python -m ccai.restclient.twingate._codegen.import_schema\n"
33
+ '"""\n'
34
+ )
35
+
36
+
37
+ def resolve_type_expression(*, type_ref: Dict[str, Any]) -> str:
38
+ """
39
+ Resolve a GraphQL type reference into an emitted Python type
40
+ expression, applying the ``NON_NULL`` and ``LIST`` wrapping rules.
41
+
42
+ :param type_ref: The GraphQL type reference node.
43
+
44
+ :returns: The Python type expression string.
45
+ """
46
+ kind = type_ref.get("kind")
47
+ name = type_ref.get("name")
48
+ of_type = type_ref.get("ofType")
49
+
50
+ if kind == "NON_NULL":
51
+ # NON_NULL drops the Optional wrapper — emit the inner type bare.
52
+ rtnval = resolve_type_expression(type_ref=of_type)
53
+ return rtnval
54
+
55
+ if kind == "LIST":
56
+ inner = resolve_type_expression(type_ref=of_type)
57
+ rtnval = f"List[{inner}]"
58
+ return rtnval
59
+
60
+ if kind == "SCALAR":
61
+ rtnval = python_typename_for_scalar(scalar_name=name)
62
+ return rtnval
63
+
64
+ if kind == "ENUM":
65
+ rtnval = name
66
+ return rtnval
67
+
68
+ if kind == "OBJECT" or kind == "INPUT_OBJECT":
69
+ rtnval = name
70
+ return rtnval
71
+
72
+ rtnval = "Any"
73
+ return rtnval
74
+
75
+
76
+ def emit_enum(*, type_node: Dict[str, Any]) -> str:
77
+ """
78
+ Emit Python source for a GraphQL ``ENUM`` type.
79
+
80
+ :param type_node: The ``__schema`` type node with ``kind=ENUM``.
81
+
82
+ :returns: Python source text for an :class:`enum.Enum` subclass.
83
+ """
84
+ name = type_node.get("name")
85
+ lines: List[str] = [GENERATED_HEADER, "import enum", "", "",
86
+ f"class {name}(enum.Enum):",
87
+ f' """Mirror of the Twingate {name} GraphQL '
88
+ f'enum."""']
89
+
90
+ enum_values = type_node.get("enumValues", [])
91
+ if enum_values is None:
92
+ enum_values = []
93
+
94
+ for value_node in enum_values:
95
+ value_name = value_node.get("name")
96
+ lines.append(f' {value_name} = "{value_name}"')
97
+
98
+ lines.append("")
99
+ lines.append(f'__all__ = ["{name}"]')
100
+ lines.append("")
101
+
102
+ rtnval = "\n".join(lines)
103
+ return rtnval
104
+
105
+
106
+ def emit_object(*, type_node: Dict[str, Any]) -> str:
107
+ """
108
+ Emit Python source for a GraphQL ``OBJECT`` or ``INPUT_OBJECT``
109
+ type as a :class:`SchemaObject` subclass.
110
+
111
+ :param type_node: The ``__schema`` type node.
112
+
113
+ :returns: Python source text for a :class:`SchemaObject` subclass.
114
+ """
115
+ name = type_node.get("name")
116
+ lines: List[str] = [
117
+ GENERATED_HEADER,
118
+ "from typing import Any, Dict, List",
119
+ "",
120
+ "from ccai.restclient.base.schemaobject import SchemaObject",
121
+ "from ccai.restclient.base.schemafield import Field",
122
+ "",
123
+ "",
124
+ f"class {name}(SchemaObject):",
125
+ f' """Mirror of the Twingate {name} GraphQL type."""',
126
+ "",
127
+ ]
128
+
129
+ fields = type_node.get("fields")
130
+ if fields is None:
131
+ fields = type_node.get("inputFields", [])
132
+ if fields is None:
133
+ fields = []
134
+
135
+ for field_node in fields:
136
+ field_name = field_node.get("name")
137
+ type_ref = field_node.get("type", {})
138
+ type_expr = resolve_type_expression(type_ref=type_ref)
139
+ required = type_ref.get("kind") == "NON_NULL"
140
+ lines.append(
141
+ f" {field_name}: {type_expr} = Field("
142
+ f"{type_expr!r}, required={required})")
143
+
144
+ lines.append("")
145
+ lines.append(f'__all__ = ["{name}"]')
146
+ lines.append("")
147
+
148
+ rtnval = "\n".join(lines)
149
+ return rtnval
150
+
151
+
152
+ def emit_schema_modules(*, schema: Dict[str, Any]) -> Dict[str, str]:
153
+ """
154
+ Walk a GraphQL ``__schema`` payload and emit a mapping of module
155
+ file name to Python source text for every non-internal OBJECT,
156
+ INPUT_OBJECT and ENUM type.
157
+
158
+ :param schema: The ``__schema`` block of an introspection payload.
159
+
160
+ :returns: A mapping of ``<typename-lowercase>.py`` to source text.
161
+ """
162
+ emitted: Dict[str, str] = {}
163
+
164
+ types = schema.get("types", [])
165
+ if types is None:
166
+ types = []
167
+
168
+ for type_node in types:
169
+ name = type_node.get("name", "")
170
+ kind = type_node.get("kind", "")
171
+ if name.startswith("__"):
172
+ continue
173
+
174
+ if kind == "ENUM":
175
+ emitted[f"{name.lower()}.py"] = emit_enum(type_node=type_node)
176
+ elif kind == "OBJECT" or kind == "INPUT_OBJECT":
177
+ emitted[f"{name.lower()}.py"] = emit_object(type_node=type_node)
178
+
179
+ rtnval = emitted
180
+ return rtnval
181
+
182
+
183
+ __all__ = [
184
+ "GENERATED_HEADER",
185
+ "resolve_type_expression",
186
+ "emit_enum",
187
+ "emit_object",
188
+ "emit_schema_modules",
189
+ ]
@@ -0,0 +1,182 @@
1
+ """
2
+ .. module:: import_schema
3
+ :platform: Darwin, Linux, Unix, Windows
4
+ :synopsis: Maintainer-facing GraphQL introspection import script for
5
+ the Twingate Admin API schema.
6
+
7
+ Run by hand against a real Twingate network when the upstream schema
8
+ changes::
9
+
10
+ python -m ccai.restclient.twingate._codegen.import_schema \\
11
+ --network <slug> --token <api-token>
12
+
13
+ See the "Codegen Decision" section of ``AGENTS.md`` — at the time of
14
+ package authoring no live Twingate network was available, so the
15
+ generated output under ``model/``, ``inputs/``, ``queries/`` and
16
+ ``mutations/`` is hand-authored from the documented Twingate GraphQL
17
+ surface. This script remains the mechanical re-sync path for a
18
+ maintainer who does have a live network.
19
+
20
+ .. moduleauthor:: Myron Walker <myron.walker@gmail.com>
21
+ """
22
+
23
+ __author__ = "Myron Walker"
24
+ __copyright__ = "Copyright 2025, Code Craft AI LLC"
25
+ __credits__ = []
26
+
27
+
28
+ import argparse
29
+ import hashlib
30
+ import json
31
+ import os
32
+
33
+ from http import HTTPStatus
34
+ from typing import Any, Dict
35
+
36
+ import requests
37
+
38
+ from ccai.errors.exceptions import ConfigurationError
39
+
40
+
41
+ # The canonical GraphQL introspection query. Trimmed to the fields the
42
+ # emitter consumes.
43
+ INTROSPECTION_QUERY = (
44
+ "query IntrospectionQuery {"
45
+ " __schema {"
46
+ " queryType { name } mutationType { name }"
47
+ " types {"
48
+ " kind name"
49
+ " fields { name type { kind name ofType { kind name"
50
+ " ofType { kind name } } } }"
51
+ " enumValues { name }"
52
+ " inputFields { name type { kind name ofType { kind name } } }"
53
+ " } } }"
54
+ )
55
+
56
+
57
+ def _spec_directory() -> str:
58
+ """
59
+ Resolve the absolute path of the ``_spec`` directory.
60
+
61
+ :returns: The ``_spec`` directory path.
62
+ """
63
+ here = os.path.dirname(os.path.abspath(__file__))
64
+ package_dir = os.path.dirname(here)
65
+ rtnval = os.path.join(package_dir, "_spec")
66
+ return rtnval
67
+
68
+
69
+ def introspect(*, network_slug: str, api_token: str,
70
+ timeout_seconds: float = 30.0) -> Dict[str, Any]:
71
+ """
72
+ POST the canonical GraphQL introspection query to a live Twingate
73
+ network and return the decoded ``__schema`` payload.
74
+
75
+ :param network_slug: The Twingate network slug.
76
+ :param api_token: The Twingate service-account API token.
77
+ :param timeout_seconds: HTTP request timeout.
78
+
79
+ :returns: The decoded GraphQL introspection response.
80
+
81
+ :raises: :class:`ConfigurationError` when the upstream response is
82
+ non-200 or carries GraphQL ``errors``.
83
+ """
84
+ endpoint = f"https://{network_slug}.twingate.com/api/graphql/v1"
85
+
86
+ headers = {
87
+ "Authorization": f"Bearer {api_token}",
88
+ "X-Network-Slug": network_slug,
89
+ "Content-Type": "application/json",
90
+ "Accept": "application/json",
91
+ }
92
+ body = {"query": INTROSPECTION_QUERY, "operationName": "IntrospectionQuery"}
93
+
94
+ response = requests.post(
95
+ endpoint, headers=headers, json=body, timeout=timeout_seconds)
96
+
97
+ if response.status_code != HTTPStatus.OK:
98
+ err_msg_lines = [
99
+ "Twingate GraphQL introspection returned a non-200 status.",
100
+ f"NETWORK SLUG: {network_slug}",
101
+ f"STATUS: {response.status_code}",
102
+ f"BODY: {response.text[:512]}",
103
+ ]
104
+ err_msg = os.linesep.join(err_msg_lines)
105
+ raise ConfigurationError(err_msg)
106
+
107
+ payload = response.json()
108
+
109
+ errors = payload.get("errors")
110
+ if errors is not None and len(errors) > 0:
111
+ err_msg_lines = [
112
+ "Twingate GraphQL introspection returned GraphQL errors.",
113
+ f"NETWORK SLUG: {network_slug}",
114
+ f"ERRORS: {errors}",
115
+ ]
116
+ err_msg = os.linesep.join(err_msg_lines)
117
+ raise ConfigurationError(err_msg)
118
+
119
+ rtnval = payload
120
+ return rtnval
121
+
122
+
123
+ def save_schema(*, payload: Dict[str, Any]) -> str:
124
+ """
125
+ Save an introspection payload to ``_spec/schema.json`` and record
126
+ its SHA-256 in ``_spec/schema.sha256``.
127
+
128
+ :param payload: The introspection payload to persist.
129
+
130
+ :returns: The computed SHA-256 hex digest.
131
+ """
132
+ spec_dir = _spec_directory()
133
+ if not os.path.isdir(spec_dir):
134
+ os.makedirs(spec_dir)
135
+
136
+ schema_text = json.dumps(payload, indent=2, sort_keys=True)
137
+ schema_bytes = schema_text.encode("utf-8")
138
+
139
+ digest = hashlib.sha256(schema_bytes).hexdigest()
140
+
141
+ schema_path = os.path.join(spec_dir, "schema.json")
142
+ with open(schema_path, "w", encoding="utf-8") as schema_file:
143
+ schema_file.write(schema_text)
144
+
145
+ hash_path = os.path.join(spec_dir, "schema.sha256")
146
+ with open(hash_path, "w", encoding="utf-8") as hash_file:
147
+ hash_file.write(digest)
148
+
149
+ rtnval = digest
150
+ return rtnval
151
+
152
+
153
+ def main() -> int:
154
+ """
155
+ Command-line entry point for the introspection import script.
156
+
157
+ :returns: Process exit code — ``0`` on success.
158
+ """
159
+ parser = argparse.ArgumentParser(
160
+ description="Introspect a live Twingate network and dump the "
161
+ "GraphQL schema.")
162
+ parser.add_argument(
163
+ "--network", required=True, help="Twingate network slug.")
164
+ parser.add_argument(
165
+ "--token", required=True,
166
+ help="Twingate service-account API token.")
167
+ args = parser.parse_args()
168
+
169
+ payload = introspect(network_slug=args.network, api_token=args.token)
170
+ digest = save_schema(payload=payload)
171
+
172
+ print(f"Twingate schema saved. SHA-256: {digest}")
173
+
174
+ rtnval = 0
175
+ return rtnval
176
+
177
+
178
+ if __name__ == "__main__":
179
+ raise SystemExit(main())
180
+
181
+
182
+ __all__ = ["INTROSPECTION_QUERY", "introspect", "save_schema", "main"]
@@ -0,0 +1,76 @@
1
+ """
2
+ .. module:: typemap
3
+ :platform: Darwin, Linux, Unix, Windows
4
+ :synopsis: Module that maps GraphQL scalar type names to Python types
5
+ for the schema-import emitter.
6
+
7
+ .. moduleauthor:: Myron Walker <myron.walker@gmail.com>
8
+ """
9
+
10
+ __author__ = "Myron Walker"
11
+ __copyright__ = "Copyright 2025, Code Craft AI LLC"
12
+ __credits__ = []
13
+
14
+
15
+ import datetime
16
+
17
+ from typing import Any, Dict
18
+
19
+
20
+ # GraphQL scalar type name -> (python type object, python type source name).
21
+ GRAPHQL_SCALAR_TYPEMAP: Dict[str, Any] = {
22
+ "String": str,
23
+ "Int": int,
24
+ "Boolean": bool,
25
+ "ID": str,
26
+ "Float": float,
27
+ "DateTime": datetime.datetime,
28
+ "JSON": dict,
29
+ }
30
+
31
+
32
+ # GraphQL scalar type name -> python type expression as used in emitted code.
33
+ GRAPHQL_SCALAR_TYPENAMES: Dict[str, str] = {
34
+ "String": "str",
35
+ "Int": "int",
36
+ "Boolean": "bool",
37
+ "ID": "str",
38
+ "Float": "float",
39
+ "DateTime": "datetime.datetime",
40
+ "JSON": "Dict[str, Any]",
41
+ }
42
+
43
+
44
+ def python_type_for_scalar(*, scalar_name: str) -> Any:
45
+ """
46
+ Resolve the Python type object for a GraphQL scalar type name.
47
+
48
+ :param scalar_name: The GraphQL scalar type name.
49
+
50
+ :returns: The Python type object, defaulting to :class:`str` for
51
+ unrecognised scalar names so the emitter never fails closed.
52
+ """
53
+ rtnval = GRAPHQL_SCALAR_TYPEMAP.get(scalar_name, str)
54
+ return rtnval
55
+
56
+
57
+ def python_typename_for_scalar(*, scalar_name: str) -> str:
58
+ """
59
+ Resolve the emitted Python type expression for a GraphQL scalar
60
+ type name.
61
+
62
+ :param scalar_name: The GraphQL scalar type name.
63
+
64
+ :returns: The Python type expression string, defaulting to ``str``
65
+ for unrecognised scalar names.
66
+ """
67
+ rtnval = GRAPHQL_SCALAR_TYPENAMES.get(scalar_name, "str")
68
+ return rtnval
69
+
70
+
71
+ __all__ = [
72
+ "GRAPHQL_SCALAR_TYPEMAP",
73
+ "GRAPHQL_SCALAR_TYPENAMES",
74
+ "python_type_for_scalar",
75
+ "python_typename_for_scalar",
76
+ ]
@@ -0,0 +1,32 @@
1
+ # `_spec/` — Twingate GraphQL Schema Snapshot
2
+
3
+ This directory is the home of the checked-in Twingate GraphQL introspection
4
+ result (`schema.json`) and its integrity hash (`schema.sha256`).
5
+
6
+ ## Codegen status
7
+
8
+ Per the §9.2a decision rule in `PLAN-VPN-AND-CLOUD-PROVISIONING.md` and the
9
+ "Codegen Decision" section of the repository `AGENTS.md`: at package-authoring
10
+ time no live Twingate network + API token was available, so GraphQL
11
+ introspection could **not** be run. The typed DTOs, inputs, query builders and
12
+ mutation builders under `model/`, `inputs/`, `queries/` and `mutations/` were
13
+ **hand-authored** from the documented Twingate GraphQL surface (and validated
14
+ against the working M3 `TwingateControlPlane`).
15
+
16
+ `schema.json` here is a **representative hand-authored snapshot** covering the
17
+ type surface the client consumes (`Resource`, `Group`, `Connector`,
18
+ `RemoteNetwork`, `Device`, `ConnectorState`, `ProtocolsInput`). It is *not* a
19
+ full live introspection dump.
20
+
21
+ ## Regenerating from a live network
22
+
23
+ A maintainer with a live Twingate network can produce the authoritative dump:
24
+
25
+ ```
26
+ python -m ccai.restclient.twingate._codegen.import_schema \
27
+ --network <slug> --token <api-token>
28
+ ```
29
+
30
+ That rewrites `schema.json` + `schema.sha256` and the emitter
31
+ (`_codegen/emitter.py`) can then be used to validate or extend the hand-authored
32
+ modules.