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.
- ccai/restclient/twingate/__init__.py +34 -0
- ccai/restclient/twingate/_codegen/__init__.py +12 -0
- ccai/restclient/twingate/_codegen/emitter.py +189 -0
- ccai/restclient/twingate/_codegen/import_schema.py +182 -0
- ccai/restclient/twingate/_codegen/typemap.py +76 -0
- ccai/restclient/twingate/_spec/README.md +32 -0
- ccai/restclient/twingate/_spec/schema.json +230 -0
- ccai/restclient/twingate/_spec/schema.sha256 +1 -0
- ccai/restclient/twingate/acltranslator/__init__.py +37 -0
- ccai/restclient/twingate/acltranslator/differ.py +154 -0
- ccai/restclient/twingate/acltranslator/executor.py +172 -0
- ccai/restclient/twingate/acltranslator/intermediate.py +76 -0
- ccai/restclient/twingate/acltranslator/parser.py +305 -0
- ccai/restclient/twingate/acltranslator/plan.py +80 -0
- ccai/restclient/twingate/apigroups/__init__.py +29 -0
- ccai/restclient/twingate/apigroups/connectorsgroup.py +367 -0
- ccai/restclient/twingate/apigroups/devicesgroup.py +248 -0
- ccai/restclient/twingate/apigroups/graphqlsupport.py +171 -0
- ccai/restclient/twingate/apigroups/groupsgroup.py +254 -0
- ccai/restclient/twingate/apigroups/remotenetworksgroup.py +266 -0
- ccai/restclient/twingate/apigroups/resourcesgroup.py +373 -0
- ccai/restclient/twingate/aspectstwingate.py +115 -0
- ccai/restclient/twingate/exceptions.py +229 -0
- ccai/restclient/twingate/graphqlrequestbody.py +61 -0
- ccai/restclient/twingate/inputs/__init__.py +22 -0
- ccai/restclient/twingate/inputs/resourceprotocolsinput.py +42 -0
- ccai/restclient/twingate/model/__init__.py +34 -0
- ccai/restclient/twingate/model/connectorstate.py +34 -0
- ccai/restclient/twingate/model/twingateconnector.py +40 -0
- ccai/restclient/twingate/model/twingatedevice.py +50 -0
- ccai/restclient/twingate/model/twingategroup.py +38 -0
- ccai/restclient/twingate/model/twingateremotenetwork.py +38 -0
- ccai/restclient/twingate/model/twingateresource.py +47 -0
- ccai/restclient/twingate/mutations/__init__.py +62 -0
- ccai/restclient/twingate/mutations/build_connector_mutations.py +109 -0
- ccai/restclient/twingate/mutations/build_device_mutations.py +46 -0
- ccai/restclient/twingate/mutations/build_group_mutations.py +102 -0
- ccai/restclient/twingate/mutations/build_remotenetwork_mutations.py +114 -0
- ccai/restclient/twingate/mutations/build_resource_mutations.py +119 -0
- ccai/restclient/twingate/mutations/build_resourceaccess_mutations.py +88 -0
- ccai/restclient/twingate/py.typed +0 -0
- ccai/restclient/twingate/queries/__init__.py +44 -0
- ccai/restclient/twingate/queries/build_connectors.py +56 -0
- ccai/restclient/twingate/queries/build_devices.py +55 -0
- ccai/restclient/twingate/queries/build_groups.py +55 -0
- ccai/restclient/twingate/queries/build_node.py +145 -0
- ccai/restclient/twingate/queries/build_remotenetworks.py +56 -0
- ccai/restclient/twingate/queries/build_resources.py +57 -0
- ccai/restclient/twingate/queries/buildersupport.py +48 -0
- ccai/restclient/twingate/twingateclient.py +630 -0
- codecraftai_restclient_twingate-1.2.0.dist-info/METADATA +100 -0
- codecraftai_restclient_twingate-1.2.0.dist-info/RECORD +54 -0
- codecraftai_restclient_twingate-1.2.0.dist-info/WHEEL +4 -0
- 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.
|