enigma-api-client 0.3.1__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 (63) hide show
  1. enigma_api_client/__init__.py +16 -0
  2. enigma_api_client/client.py +134 -0
  3. enigma_api_client/client_inject_id_and_typename_helper.py +248 -0
  4. enigma_api_client/client_inject_urls_helper.py +26 -0
  5. enigma_api_client/plugins/.DS_Store +0 -0
  6. enigma_api_client/plugins/enigma-api/.claude-plugin/plugin.json +8 -0
  7. enigma_api_client/plugins/enigma-api/README.md +82 -0
  8. enigma_api_client/plugins/enigma-api/harness/conftest.py +42 -0
  9. enigma_api_client/plugins/enigma-api/harness/test_linter.py +863 -0
  10. enigma_api_client/plugins/enigma-api/harness/test_queries.py +143 -0
  11. enigma_api_client/plugins/enigma-api/harness/test_skill_rules.py +351 -0
  12. enigma_api_client/plugins/enigma-api/pyproject.toml +9 -0
  13. enigma_api_client/plugins/enigma-api/scripts/__pycache__/lint_query.cpython-313.pyc +0 -0
  14. enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-313.pyc +0 -0
  15. enigma_api_client/plugins/enigma-api/scripts/__pycache__/query_validator.cpython-314.pyc +0 -0
  16. enigma_api_client/plugins/enigma-api/scripts/generate_report.py +384 -0
  17. enigma_api_client/plugins/enigma-api/scripts/lint_query.py +712 -0
  18. enigma_api_client/plugins/enigma-api/scripts/query_validator.py +352 -0
  19. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/SKILL.md +620 -0
  20. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/03_brand_profile.md +163 -0
  21. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/brand/13_brand_dump.md +95 -0
  22. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/derived/07_coalesce_ein_formation.md +146 -0
  23. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/01_coffee_shops_zip.md +287 -0
  24. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/discovery/11_ol_industry_discovery.md +116 -0
  25. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/export/08_segment_export.md +96 -0
  26. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/02_officers_lookup.md +366 -0
  27. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/kyb/10_kyb_verify.md +176 -0
  28. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/legal_entity/09_legal_entity_profile.md +97 -0
  29. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/locations/05_brand_locations_by_state.md +120 -0
  30. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/person_search/04_find_person_companies.md +160 -0
  31. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/revenue/06_brand_revenue.md +176 -0
  32. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/examples/risk/12_risk_signals.md +118 -0
  33. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/card_transactions.md +91 -0
  34. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/field_corrections.json +106 -0
  35. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/filter_syntax.md +203 -0
  36. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/naics_2022_mapping.json +184 -0
  37. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/references/pagination_template.py +115 -0
  38. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/schema.graphql +44971 -0
  39. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/test_examples.py +1122 -0
  40. enigma_api_client/plugins/enigma-api/skills/enigma-graphql/scripts/validate_query.py +177 -0
  41. enigma_api_client/plugins/enigma-api/test-cases/graphql-test-cases.md +196 -0
  42. enigma_api_client/plugins/enigma-api/tests/accuracy_cases.csv +116 -0
  43. enigma_api_client/plugins/enigma-api/tests/applicability_review.csv +64 -0
  44. enigma_api_client/plugins/enigma-api/tests/parallel-ground-truth.csv +101 -0
  45. enigma_api_client/plugins/enigma-api/tests/run_tests.sh +320 -0
  46. enigma_api_client/plugins/enigma-api/tests/test_cases.yaml +213 -0
  47. enigma_api_client/plugins/enigma-api/tests/validation_cases.csv +101 -0
  48. enigma_api_client/py.typed +0 -0
  49. enigma_api_client/query_router/__init__.py +3 -0
  50. enigma_api_client/query_router/geo.py +121 -0
  51. enigma_api_client/query_router/templates.py +475 -0
  52. enigma_api_client/strategies/__init__.py +16 -0
  53. enigma_api_client/strategies/agent.py +63 -0
  54. enigma_api_client/strategies/base.py +15 -0
  55. enigma_api_client/strategies/generative.py +143 -0
  56. enigma_api_client/strategies/llm_provider.py +137 -0
  57. enigma_api_client/strategies/service.py +103 -0
  58. enigma_api_client/strategies/template.py +189 -0
  59. enigma_api_client-0.3.1.dist-info/METADATA +257 -0
  60. enigma_api_client-0.3.1.dist-info/RECORD +63 -0
  61. enigma_api_client-0.3.1.dist-info/WHEEL +5 -0
  62. enigma_api_client-0.3.1.dist-info/licenses/LICENSE +201 -0
  63. enigma_api_client-0.3.1.dist-info/top_level.txt +5 -0
@@ -0,0 +1,16 @@
1
+ """enigma_api_client - Enigma's Public API SDK"""
2
+
3
+ from typing import Sequence
4
+
5
+ from enigma_api_client.client import Client, ClientConfig, SearchResponse, EnigmaClientException, EnigmaClientErrorCode
6
+ from enigma_api_client.strategies.base import QueryStrategy
7
+
8
+ __version__ = "0.3.1"
9
+ __all__: Sequence[str] = [
10
+ "Client",
11
+ "ClientConfig",
12
+ "SearchResponse",
13
+ "EnigmaClientException",
14
+ "EnigmaClientErrorCode",
15
+ "QueryStrategy",
16
+ ]
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from typing import Any, TYPE_CHECKING
7
+
8
+ import httpx
9
+
10
+ from enigma_api_client.client_inject_id_and_typename_helper import inject_fields_in_query
11
+ from enigma_api_client.client_inject_urls_helper import inject_urls
12
+
13
+ if TYPE_CHECKING:
14
+ from enigma_api_client.strategies.base import QueryStrategy
15
+
16
+
17
+ class EnigmaClientErrorCode(Enum):
18
+ INVALID_QUERY = "INVALID_GRAPHQL_QUERY"
19
+ INVALID_VARIABLES = "INVALID_VARIABLES"
20
+ GENERATE_QUERY_CALLBACK_NOT_CONFIGURED = "GENERATE_QUERY_CALLBACK_NOT_CONFIGURED"
21
+
22
+ class EnigmaClientException(Exception):
23
+ def __init__(self, error_code: EnigmaClientErrorCode, message: str):
24
+ self.error_code = error_code
25
+ super().__init__(message)
26
+
27
+
28
+ @dataclass
29
+ class ClientConfig:
30
+ # Enigma API key
31
+ enigma_api_key: str
32
+
33
+ # Pluggable query strategy for generating GraphQL from natural language prompts.
34
+ # Available strategies: ServiceStrategy (default), TemplateStrategy, GenerativeStrategy, AgentStrategy
35
+ # Default: ServiceStrategy (delegates to the Enigma Semantic Transformation Service)
36
+ query_strategy: QueryStrategy | None = None
37
+
38
+ # If True, automatically downloads new skills from Enigma at client initialization and every 24 hours.
39
+ # If False, will only use static skills packaged with the client.
40
+ # Default: True
41
+ auto_download_new_skills: bool = True
42
+
43
+ # Enigma GraphQL API endpoint
44
+ url: str = "https://api.enigma.com/graphql"
45
+
46
+ # Auto Inject ID and typename in query. This is required in order to get __url fields in the response.
47
+ auto_inject_id_and_typename: bool = True
48
+
49
+
50
+ @dataclass
51
+ class SearchResponse:
52
+ status_code: int
53
+ errors: Any = None
54
+ data: dict[str, Any] | None = None
55
+ extensions: Any = None
56
+
57
+ def is_success(self) -> bool:
58
+ return 200 <= self.status_code <= 299 and not self.errors
59
+
60
+ class Client:
61
+ def __init__(self, config: ClientConfig):
62
+ self._config = config
63
+
64
+ async def search(self, *, prompt: str | None = None, query: str | None = None, variables: dict[str, Any] | None = None) -> SearchResponse:
65
+ """
66
+ Executes a GraphQL request against Enigma GraphQL API
67
+
68
+ :param prompt: will use prompt to generate a query using the configured query_strategy.
69
+ If query_strategy is NOT configured on ClientConfig, an exception with the error code
70
+ "GENERATE_QUERY_CALLBACK_NOT_CONFIGURED" will be raised.
71
+ Cannot be used together with query or variables parameters.
72
+ :param query: A string representing the GraphQL query. Cannot be used together with the prompt parameter.
73
+ :param variables: A dict representing the GraphQL variables. Cannot be used together with the prompt parameter.
74
+ :return: `SearchResponse` containing the response of the search.
75
+ """
76
+
77
+ if prompt and (query or variables):
78
+ raise EnigmaClientException(EnigmaClientErrorCode.INVALID_QUERY, "Cannot provide both query/variable and prompt parameters")
79
+
80
+ if not prompt and not query and not variables:
81
+ raise EnigmaClientException(EnigmaClientErrorCode.INVALID_QUERY, "Invalid query. Must provide either query/variable or prompt parameters")
82
+
83
+ if prompt is not None:
84
+ strategy = self._config.query_strategy
85
+ if strategy is None:
86
+ from enigma_api_client.strategies.service import ServiceStrategy
87
+ strategy = ServiceStrategy(enigma_api_key=self._config.enigma_api_key, query_strategy="template")
88
+ self._config.query_strategy = strategy
89
+ query, variables = await strategy.generate_query(prompt)
90
+
91
+ return await self._execute_graphql_request(query, variables)
92
+
93
+ async def _execute_graphql_request(self, query: str | None, variables: dict[str, Any] | None) -> SearchResponse:
94
+ headers = {
95
+ "Content-Type": "application/json",
96
+ "x-api-key": self._config.enigma_api_key or os.getenv("ENIGMA_API_KEY", ""),
97
+ }
98
+
99
+ payload = {
100
+ "query": query,
101
+ "variables": variables or {},
102
+ }
103
+ if self._config.auto_inject_id_and_typename:
104
+ payload = self._inject_id_and_typename_whereever_needed(payload)
105
+
106
+ async with httpx.AsyncClient() as client:
107
+ response = await client.post(
108
+ self._config.url,
109
+ json=payload,
110
+ headers=headers,
111
+ timeout=30.0
112
+ )
113
+ status_code = response.status_code
114
+ resp_json = response.json()
115
+
116
+ return SearchResponse(
117
+ data=inject_urls(resp_json.get("data")),
118
+ errors=resp_json.get("errors"),
119
+ extensions=resp_json.get("extensions"),
120
+ status_code=status_code
121
+ )
122
+
123
+ def _inject_id_and_typename_whereever_needed(self, payload: dict[str, Any]) -> dict[str, Any]:
124
+ query = payload.get("query")
125
+ if not query or not isinstance(query, str):
126
+ return payload
127
+ try:
128
+ new_query = inject_fields_in_query(query)
129
+ except Exception:
130
+ return payload # never break on a parse failure
131
+ if new_query == query:
132
+ return payload
133
+ return {**payload, "query": new_query}
134
+
@@ -0,0 +1,248 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ # ── GraphQL field injector ─────────────────────────────────────────────────────
6
+ #
7
+ # Parses the raw query string and injects `id` and `__typename` into every
8
+ # selection set that opens an inline fragment (`... on TypeName { … }`) or a
9
+ # connection node (`edges { node { … } }`), but only when those fields are not
10
+ # already present. All other selection sets are left untouched.
11
+ #
12
+ # Implementation is a hand-rolled tokeniser + single-pass walker so that we have
13
+ # zero extra dependencies. The original query string is only modified at the
14
+ # exact character positions that need an injection; everything else is preserved
15
+ # verbatim (whitespace, comments, aliases, arguments, etc.).
16
+
17
+ # Token kinds: 'ws' | 'comment' | 'string' | 'spread' | 'punct' | 'name' | 'num'
18
+ _Token = tuple[str, str, int] # (kind, value, end_pos) – end_pos is exclusive
19
+
20
+
21
+ def _gql_tokenize(query: str) -> list[_Token]:
22
+ """Tokenise a GraphQL query string into (kind, value, end_pos) triples."""
23
+ tokens: list[_Token] = []
24
+ i = 0
25
+ n = len(query)
26
+ while i < n:
27
+ c = query[i]
28
+ # Block string """…"""
29
+ if query[i:i+3] == '"""':
30
+ j = query.find('"""', i + 3)
31
+ j = (j + 3) if j != -1 else n
32
+ tokens.append(('string', query[i:j], j)); i = j
33
+ # Regular string "…"
34
+ elif c == '"':
35
+ j = i + 1
36
+ while j < n:
37
+ if query[j] == '\\': j += 2
38
+ elif query[j] == '"': j += 1; break
39
+ else: j += 1
40
+ tokens.append(('string', query[i:j], j)); i = j
41
+ # Line comment #…
42
+ elif c == '#':
43
+ j = query.find('\n', i); j = j if j != -1 else n
44
+ tokens.append(('comment', query[i:j], j)); i = j
45
+ # Insignificant whitespace and commas
46
+ elif c in ' \t\n\r,':
47
+ j = i + 1
48
+ while j < n and query[j] in ' \t\n\r,': j += 1
49
+ tokens.append(('ws', query[i:j], j)); i = j
50
+ # Fragment spread operator
51
+ elif query[i:i+3] == '...':
52
+ tokens.append(('spread', '...', i + 3)); i += 3
53
+ # Single-character punctuation
54
+ elif c in '{}()[]!$:=@|&':
55
+ tokens.append(('punct', c, i + 1)); i += 1
56
+ # Identifiers [_A-Za-z][_A-Za-z0-9]*
57
+ elif c.isalpha() or c == '_':
58
+ j = i + 1
59
+ while j < n and (query[j].isalnum() or query[j] == '_'): j += 1
60
+ tokens.append(('name', query[i:j], j)); i = j
61
+ # Numbers (treated as opaque)
62
+ elif c.isdigit() or (c == '-' and i + 1 < n and query[i + 1].isdigit()):
63
+ j = i + 1
64
+ while j < n and (query[j].isdigit() or query[j] in '.eE+-'): j += 1
65
+ tokens.append(('num', query[i:j], j)); i = j
66
+ else:
67
+ tokens.append(('num', c, i + 1)); i += 1
68
+ return tokens
69
+
70
+
71
+ def _selection_field_names(tokens: list[_Token], brace_idx: int) -> set[str]:
72
+ """
73
+ Return the set of field names selected at depth-1 inside the selection set
74
+ opened by tokens[brace_idx] (which must be a '{' punct token).
75
+
76
+ Handles: nested braces, argument lists (parens), inline fragments (skipped),
77
+ named fragment spreads (skipped), aliases (the alias is skipped; the actual
78
+ field name is collected).
79
+ """
80
+ n = len(tokens)
81
+ depth = 0 # brace depth relative to the opening '{'
82
+ paren_depth = 0
83
+ fields: set[str] = set()
84
+ # Last two non-ws tokens seen (for pattern matching)
85
+ recent: list[tuple[str, str]] = []
86
+
87
+ def push(k: str, v: str) -> None:
88
+ recent.append((k, v))
89
+ if len(recent) > 2:
90
+ recent.pop(0)
91
+
92
+ i = brace_idx
93
+ while i < n:
94
+ kind, value, _ = tokens[i]
95
+
96
+ if kind in ('ws', 'comment'):
97
+ i += 1; continue
98
+
99
+ # Skip everything inside argument lists
100
+ if paren_depth > 0:
101
+ if kind == 'punct' and value == '(': paren_depth += 1
102
+ elif kind == 'punct' and value == ')': paren_depth -= 1
103
+ i += 1; continue
104
+
105
+ if kind == 'punct' and value == '(':
106
+ paren_depth += 1; i += 1; continue
107
+
108
+ if kind == 'punct' and value == '{':
109
+ depth += 1; push(kind, value); i += 1; continue
110
+
111
+ if kind == 'punct' and value == '}':
112
+ depth -= 1
113
+ if depth == 0: break
114
+ push(kind, value); i += 1; continue
115
+
116
+ if depth == 1 and kind == 'name':
117
+ prev = recent[-1] if recent else None
118
+ # Skip: type name after 'on' (... on TypeName)
119
+ if prev == ('name', 'on'):
120
+ push(kind, value); i += 1; continue
121
+ # Skip: named fragment reference ...FragmentName
122
+ if prev == ('spread', '...'):
123
+ push(kind, value); i += 1; continue
124
+ # Skip: alias name (alias: fieldName — collect fieldName on next iter via ':')
125
+ j = i + 1
126
+ next_tok: tuple[str, str] | None = None
127
+ while j < n:
128
+ if tokens[j][0] not in ('ws', 'comment'):
129
+ next_tok = (tokens[j][0], tokens[j][1]); break
130
+ j += 1
131
+ if next_tok == ('punct', ':'):
132
+ push(kind, value); i += 1; continue
133
+ # Actual field name
134
+ fields.add(value)
135
+
136
+ push(kind, value)
137
+ i += 1
138
+
139
+ return fields
140
+
141
+
142
+ def _classify_opening_brace(
143
+ recent_sig: list[tuple[str, str]],
144
+ ctx_stack: list[str],
145
+ ) -> str:
146
+ """
147
+ Classify the context of an opening '{' given the last few significant
148
+ (non-ws, non-comment) tokens that preceded it and the current context stack.
149
+
150
+ Returns one of: 'inline_fragment' | 'node_in_edges' | 'edges' | 'other'
151
+ """
152
+ r = recent_sig # most-recent last
153
+
154
+ # Inline fragment: ... on TypeName [optional directives] {
155
+ # We scan backwards for (name, 'on') preceded (somewhere) by (spread, '...')
156
+ for idx in range(len(r) - 1, -1, -1):
157
+ if r[idx] == ('name', 'on'):
158
+ # Is there a spread '...' anywhere before this 'on' in recent?
159
+ if any(t == ('spread', '...') for t in r[:idx]):
160
+ return 'inline_fragment'
161
+ break # 'on' not preceded by spread — not an inline fragment
162
+
163
+ # Find the last field name in recent (ignoring structural punctuation and 'on')
164
+ last_field: str | None = None
165
+ for kind, value in reversed(r):
166
+ if kind == 'name' and value != 'on':
167
+ last_field = value
168
+ break
169
+ if kind == 'punct' and value in ('{', '}'):
170
+ break # structural boundary — stop looking
171
+
172
+ if last_field == 'node' and ctx_stack and ctx_stack[-1] == 'edges':
173
+ return 'node_in_edges'
174
+ if last_field == 'edges':
175
+ return 'edges'
176
+ return 'other'
177
+
178
+
179
+ def _find_injections(query: str) -> list[tuple[int, list[str]]]:
180
+ """
181
+ Return a list of (insert_pos, fields) where insert_pos is the character
182
+ position immediately after a '{' that opens an injection-target selection
183
+ set, and fields is the subset of ['id', '__typename'] not already present.
184
+ """
185
+ tokens = _gql_tokenize(query)
186
+ injections: list[tuple[int, list[str]]] = []
187
+ ctx_stack: list[str] = [] # context for each open '{'
188
+ paren_depth = 0
189
+ recent_sig: list[tuple[str, str]] = [] # last ≤4 significant tokens
190
+
191
+ def push_sig(k: str, v: str) -> None:
192
+ recent_sig.append((k, v))
193
+ if len(recent_sig) > 4:
194
+ recent_sig.pop(0)
195
+
196
+ for idx, (kind, value, end_pos) in enumerate(tokens):
197
+ if kind in ('ws', 'comment'):
198
+ continue
199
+
200
+ # Inside argument lists — track depth but don't update context
201
+ if paren_depth > 0:
202
+ if kind == 'punct' and value == '(': paren_depth += 1
203
+ elif kind == 'punct' and value == ')': paren_depth -= 1
204
+ # Intentionally NOT calling push_sig here so argument content
205
+ # does not pollute the recent-token window used for classification.
206
+ continue
207
+
208
+ if kind == 'punct' and value == '(':
209
+ paren_depth += 1
210
+ continue # don't push to recent_sig either
211
+
212
+ if kind == 'punct' and value == '{':
213
+ ctx = _classify_opening_brace(recent_sig, ctx_stack)
214
+ if ctx in ('inline_fragment', 'node_in_edges'):
215
+ existing = _selection_field_names(tokens, idx)
216
+ to_inject = [f for f in ('id', '__typename') if f not in existing]
217
+ if to_inject:
218
+ injections.append((end_pos, to_inject))
219
+ ctx_stack.append(ctx)
220
+ push_sig(kind, value)
221
+ continue
222
+
223
+ if kind == 'punct' and value == '}':
224
+ if ctx_stack:
225
+ ctx_stack.pop()
226
+ push_sig(kind, value)
227
+ continue
228
+
229
+ push_sig(kind, value)
230
+
231
+ return injections
232
+
233
+
234
+ def inject_fields_in_query(query: str) -> str:
235
+ """
236
+ Return a copy of *query* with `id` and `__typename` injected into every
237
+ inline-fragment and edge-node selection set that is missing them.
238
+ Injections are applied in reverse position order to keep earlier offsets valid.
239
+ """
240
+ injections = _find_injections(query)
241
+ if not injections:
242
+ return query
243
+ # Sort descending so we insert from the back, preserving earlier positions.
244
+ injections.sort(key=lambda x: x[0], reverse=True)
245
+ result = query
246
+ for pos, fields in injections:
247
+ result = result[:pos] + ' ' + ' '.join(fields) + result[pos:]
248
+ return result
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ _PERMALINK_TYPES_TO_PATH = {
6
+ "OperatingLocation": "operating-location",
7
+ "Brand": "brand",
8
+ "LegalEntity": "legal-entity",
9
+ "Person": "person",
10
+ }
11
+ _PERMALINK_BASE = "https://console.enigma.com/explore/profile"
12
+
13
+
14
+ def inject_urls(node: Any) -> Any:
15
+ """Recursively walk a GraphQL response node and add ``__url`` to any object
16
+ that has both ``__typename`` and ``id`` and whose type is in _PERMALINK_TYPES."""
17
+ if isinstance(node, dict):
18
+ typename = node.get("__typename")
19
+ entity_id = node.get("id")
20
+ result = {k: inject_urls(v) for k, v in node.items()}
21
+ if typename and entity_id is not None and (path := _PERMALINK_TYPES_TO_PATH.get(typename)):
22
+ result["__url"] = f"{_PERMALINK_BASE}/{path}/{entity_id}"
23
+ return result
24
+ if isinstance(node, list):
25
+ return [inject_urls(item) for item in node]
26
+ return node
Binary file
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "enigma-api",
3
+ "description": "Query Enigma APIs — GraphQL business data, KYB verification, sanctions screening, and government archive records",
4
+ "version": "1.1.0",
5
+ "author": {
6
+ "name": "Jarrod Parker"
7
+ }
8
+ }
@@ -0,0 +1,82 @@
1
+ # enigma-api
2
+
3
+ Query Enigma APIs — GraphQL business data, KYB verification, sanctions screening, and government archive records. Claude automatically uses the appropriate skill based on what you're asking about.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ /plugin install enigma-api@enigma-plugins
9
+ ```
10
+
11
+ ## Prerequisites
12
+
13
+ Set your Enigma API key in your environment:
14
+
15
+ ```bash
16
+ export ENIGMA_API_KEY=your_api_key_here
17
+ ```
18
+
19
+ Get your API key from [console.enigma.com](https://console.enigma.com/).
20
+
21
+ ## Skills
22
+
23
+ ### enigma-graphql
24
+
25
+ Query the Enigma GraphQL API for business intelligence data — brands, operating locations, legal entities, corporate structure, and more.
26
+
27
+ ```
28
+ "Look up Starbucks in the Enigma API"
29
+ "Find all operating locations for Target in California"
30
+ "Search for the legal entity behind Acme Corp"
31
+ "Get the corporate structure for Nike"
32
+ ```
33
+
34
+ ### enigma-kyb
35
+
36
+ Verify businesses using the KYB (Know Your Business) API — check registration status, name verification, and due diligence.
37
+
38
+ ```
39
+ "Verify Enigma Technologies Inc as a business"
40
+ "Run KYB on Acme Corp in New York"
41
+ "Check if this business is registered: Smith & Sons LLC, 123 Main St, Chicago IL"
42
+ ```
43
+
44
+ ### enigma-screen
45
+
46
+ Screen persons and organizations against sanctions, PEP, and watchlist databases.
47
+
48
+ ```
49
+ "Screen Vladimir Putin for sanctions"
50
+ "Check if Acme International Trading is on any watchlists"
51
+ "Run a sanctions screening on John Smith"
52
+ "Screen this text for sanctioned entities: 'Payment to Bank of Russia'"
53
+ ```
54
+
55
+ ### enigma-gov-archive
56
+
57
+ Search government archive public records — violations, licenses, liens, legal actions, debarments, and contracts.
58
+
59
+ ```
60
+ "Search government records for Blue Bottle Coffee"
61
+ "Find any violations or liens for Acme Corp"
62
+ "Check government archives for Smith Construction LLC"
63
+ ```
64
+
65
+ ## Supported Entity Types (GraphQL)
66
+
67
+ | Type | What it represents |
68
+ |---|---|
69
+ | Brand | A business brand/company — industries, locations, revenue signals |
70
+ | Operating Location | A physical business location — address, phone, status, reviews |
71
+ | Legal Entity | A registered legal entity — officers, TINs, filings, registrations |
72
+ | Person | A person — associated companies, officer roles, registrations |
73
+
74
+ ## Documentation
75
+
76
+ - [Enigma API Docs](https://documentation.enigma.com/)
77
+ - [GraphQL Guide](https://documentation.enigma.com/guides/graphql)
78
+ - [API Playground](https://console.enigma.com/explore/graphql)
79
+
80
+ ## Author
81
+
82
+ Jarrod Parker
@@ -0,0 +1,42 @@
1
+ # ABOUTME: Shared pytest fixtures for the enigma-api test harness
2
+ # ABOUTME: Provides skill_path, skill_content, and graphql_blocks fixtures
3
+
4
+ """
5
+ Shared pytest fixtures for the Enigma GraphQL test harness.
6
+
7
+ Fixtures:
8
+ skill_path — absolute path to SKILL.md
9
+ skill_content — full text of SKILL.md
10
+ graphql_blocks — list of {query, variables, section, line_number} dicts
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ # Add scripts/ to path so we can import lint_query and query_validator
20
+ PLUGIN_ROOT = Path(__file__).resolve().parent.parent
21
+ sys.path.insert(0, str(PLUGIN_ROOT / "scripts"))
22
+
23
+
24
+ @pytest.fixture(scope="session")
25
+ def skill_path() -> Path:
26
+ """Path to the SKILL.md file."""
27
+ p = PLUGIN_ROOT / "skills" / "enigma-graphql" / "SKILL.md"
28
+ assert p.exists(), f"SKILL.md not found at {p}"
29
+ return p
30
+
31
+
32
+ @pytest.fixture(scope="session")
33
+ def skill_content(skill_path: Path) -> str:
34
+ """Full text content of SKILL.md."""
35
+ return skill_path.read_text(encoding="utf-8")
36
+
37
+
38
+ @pytest.fixture(scope="session")
39
+ def graphql_blocks(skill_content: str) -> list[dict]:
40
+ """All GraphQL code blocks extracted from SKILL.md with section context."""
41
+ from query_validator import extract_graphql_blocks_with_context
42
+ return extract_graphql_blocks_with_context(skill_content)