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,384 @@
1
+ # ABOUTME: Generates a concise MR-ready validation report for SKILL.md
2
+ # ABOUTME: Runs linter, query checks, and structural rules; outputs markdown summary
3
+
4
+ """
5
+ Enigma GraphQL Skill — Validation Report Generator
6
+
7
+ Runs all checks against SKILL.md and prints a concise markdown report
8
+ suitable for pasting into an MR description or CI output.
9
+
10
+ Usage:
11
+ python scripts/generate_report.py # default: skills/enigma-graphql/SKILL.md
12
+ python scripts/generate_report.py path/to/SKILL.md # custom path
13
+
14
+ Exit code: 0 if all critical checks pass, 1 otherwise.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ import sys
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+
24
+ # Add scripts/ to path
25
+ SCRIPTS_DIR = Path(__file__).resolve().parent
26
+ PLUGIN_ROOT = SCRIPTS_DIR.parent
27
+ sys.path.insert(0, str(SCRIPTS_DIR))
28
+
29
+ from lint_query import lint_query, Severity # noqa: E402
30
+ from query_validator import ( # noqa: E402
31
+ validate_query,
32
+ extract_graphql_blocks_with_context,
33
+ is_complete_query,
34
+ )
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Check definitions
39
+ # ---------------------------------------------------------------------------
40
+
41
+ STRUCTURAL_CHECKS: list[dict] = [
42
+ {
43
+ "id": "S-01",
44
+ "name": "No limit:50 in JSON blocks",
45
+ "check": lambda c: not bool(re.search(r'"limit"\s*:\s*50\b', _json_blocks(c))),
46
+ },
47
+ {
48
+ "id": "S-02",
49
+ "name": "Discovery limits ≤ 10",
50
+ "check": lambda c: _check_discovery_limits(c),
51
+ },
52
+ {
53
+ "id": "S-03",
54
+ "name": "Discovery OL first ≤ 5",
55
+ "check": lambda c: _check_discovery_ol_first(c),
56
+ },
57
+ {
58
+ "id": "S-04",
59
+ "name": "No platformBrandId in OL recipes",
60
+ "check": lambda c: "platformBrandId" not in _get_section_blocks(c, r"Operating Location.*Revenue", "all"),
61
+ },
62
+ {
63
+ "id": "S-05",
64
+ "name": "platformBrandId in Brand revenue",
65
+ "check": lambda c: "platformBrandId" in _get_section_blocks(c, r"Brand Revenue Query", "json"),
66
+ },
67
+ {
68
+ "id": "S-06",
69
+ "name": "Singular count fields in aggregate",
70
+ "check": lambda c: _check_aggregate_singular(c),
71
+ },
72
+ {
73
+ "id": "S-07",
74
+ "name": "Plural count fields in search",
75
+ "check": lambda c: _check_search_plural(c),
76
+ },
77
+ {
78
+ "id": "S-08",
79
+ "name": "@coalesce paired with @skip",
80
+ "check": lambda c: _check_coalesce_skip(c),
81
+ },
82
+ {
83
+ "id": "S-09",
84
+ "name": "street1 AGENT BEHAVIOR warning",
85
+ "check": lambda c: "AGENT BEHAVIOR" in c and "street" in c[c.index("AGENT BEHAVIOR"):c.index("AGENT BEHAVIOR") + 500].lower(),
86
+ },
87
+ {
88
+ "id": "S-10",
89
+ "name": "Franchise/chain warning present",
90
+ "check": lambda c: bool(re.search(r"Franchise.*Chain.*Resolution|Chain.*Resolution", c, re.IGNORECASE)),
91
+ },
92
+ {
93
+ "id": "S-11",
94
+ "name": "No totalCount in code blocks",
95
+ "check": lambda c: "totalCount" not in _graphql_blocks_text(c),
96
+ },
97
+ ]
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Helpers for structural checks
102
+ # ---------------------------------------------------------------------------
103
+
104
+ def _json_blocks(content: str) -> str:
105
+ """Concatenate all ```json blocks."""
106
+ return "\n".join(m.group(1) for m in re.finditer(r"^```json\s*\n(.*?)\n```", content, re.M | re.S))
107
+
108
+
109
+ def _graphql_blocks_text(content: str) -> str:
110
+ """Concatenate all ```graphql blocks."""
111
+ return "\n".join(m.group(1) for m in re.finditer(r"^```graphql\s*\n(.*?)\n```", content, re.M | re.S))
112
+
113
+
114
+ def _get_section(content: str, heading_pattern: str) -> str | None:
115
+ lines = content.split("\n")
116
+ capturing = False
117
+ captured: list[str] = []
118
+ heading_level = None
119
+ for line in lines:
120
+ hm = re.match(r"^(#{1,4})\s+(.*)", line)
121
+ if hm:
122
+ level = len(hm.group(1))
123
+ text = hm.group(2).strip()
124
+ if re.search(heading_pattern, text, re.IGNORECASE):
125
+ capturing = True
126
+ heading_level = level
127
+ captured.append(line)
128
+ continue
129
+ elif capturing and level <= heading_level:
130
+ break
131
+ if capturing:
132
+ captured.append(line)
133
+ return "\n".join(captured) if captured else None
134
+
135
+
136
+ def _get_section_blocks(content: str, heading_pattern: str, block_type: str) -> str:
137
+ sec = _get_section(content, heading_pattern)
138
+ if not sec:
139
+ return ""
140
+ if block_type == "json":
141
+ return _json_blocks(sec)
142
+ elif block_type == "graphql":
143
+ return _graphql_blocks_text(sec)
144
+ else:
145
+ return _json_blocks(sec) + "\n" + _graphql_blocks_text(sec)
146
+
147
+
148
+ def _check_discovery_limits(content: str) -> bool:
149
+ sec = _get_section(content, r"Discovery.*Find All|Discovery.*Businesses")
150
+ if not sec:
151
+ return True
152
+ for m in re.finditer(r'"limit"\s*:\s*(\d+)', _json_blocks(sec)):
153
+ if int(m.group(1)) > 10:
154
+ return False
155
+ return True
156
+
157
+
158
+ def _check_discovery_ol_first(content: str) -> bool:
159
+ sec = _get_section(content, r"Discovery.*Find All|Discovery.*Businesses")
160
+ if not sec:
161
+ return True
162
+ for m in re.finditer(r"operatingLocations\s*\(\s*first\s*:\s*(\d+)", _graphql_blocks_text(sec)):
163
+ if int(m.group(1)) > 5:
164
+ return False
165
+ return True
166
+
167
+
168
+ def _check_aggregate_singular(content: str) -> bool:
169
+ sec = _get_section(content, r"Aggregate Query")
170
+ if not sec:
171
+ return True
172
+ # Stop at Count on Search subsection
173
+ lines = sec.split("\n")
174
+ trimmed = []
175
+ for line in lines:
176
+ if re.match(r"^#{1,4}\s+Count on Search", line, re.IGNORECASE):
177
+ break
178
+ trimmed.append(line)
179
+ gql = _graphql_blocks_text("\n".join(trimmed))
180
+ for m in re.finditer(r'count\s*\(\s*field\s*:\s*"(\w+)"', gql):
181
+ if m.group(1) in ("operatingLocations", "legalEntities", "brands"):
182
+ return False
183
+ return True
184
+
185
+
186
+ def _check_search_plural(content: str) -> bool:
187
+ sec = _get_section(content, r"Count on Search")
188
+ if not sec:
189
+ return True
190
+ gql = _graphql_blocks_text(sec)
191
+ for m in re.finditer(r'count\s*\(\s*field\s*:\s*"(\w+)"', gql):
192
+ if m.group(1) in ("operatingLocation", "legalEntity", "brand"):
193
+ return False
194
+ return True
195
+
196
+
197
+ def _check_coalesce_skip(content: str) -> bool:
198
+ for m in re.finditer(r"^```graphql\s*\n(.*?)\n```", content, re.M | re.S):
199
+ block = m.group(1)
200
+ if "@coalesce" in block and "@skip(if: true)" not in block and "@skip(if:true)" not in block:
201
+ return False
202
+ return True
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # Report generator
207
+ # ---------------------------------------------------------------------------
208
+
209
+ def generate_report(skill_path: Path) -> tuple[str, bool]:
210
+ """Generate a markdown validation report. Returns (report_text, all_passed)."""
211
+ content = skill_path.read_text(encoding="utf-8")
212
+ blocks = extract_graphql_blocks_with_context(content)
213
+
214
+ lines: list[str] = []
215
+ all_passed = True
216
+
217
+ # Header
218
+ lines.append("## SKILL.md Validation Report")
219
+ lines.append("")
220
+ lines.append(f"**File**: `{skill_path.name}` | **Date**: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
221
+ lines.append("")
222
+
223
+ # ── Section 1: Structural checks ──────────────────────────────
224
+ lines.append("### Structural Checks")
225
+ lines.append("")
226
+ lines.append("| # | Check | Result |")
227
+ lines.append("|---|-------|--------|")
228
+
229
+ structural_pass = 0
230
+ structural_fail = 0
231
+ for check in STRUCTURAL_CHECKS:
232
+ try:
233
+ passed = check["check"](content)
234
+ except Exception:
235
+ passed = False
236
+ icon = "✅" if passed else "❌"
237
+ if not passed:
238
+ structural_fail += 1
239
+ all_passed = False
240
+ else:
241
+ structural_pass += 1
242
+ lines.append(f"| {check['id']} | {check['name']} | {icon} |")
243
+
244
+ lines.append("")
245
+ lines.append(f"> {structural_pass} passed, {structural_fail} failed")
246
+ lines.append("")
247
+
248
+ # ── Section 2: Query validation ───────────────────────────────
249
+ complete = [b for b in blocks if is_complete_query(b["query"])]
250
+ snippets = [b for b in blocks if not is_complete_query(b["query"])]
251
+
252
+ syntax_pass = 0
253
+ syntax_fail = 0
254
+ lint_pass = 0
255
+ lint_fail = 0
256
+ lint_failures: list[str] = []
257
+
258
+ for block in complete:
259
+ q = block["query"]
260
+ section = block.get("section", "?")
261
+ line = block.get("line_number", 0)
262
+ label = f"L{line} {section}"[:60]
263
+
264
+ # Syntax
265
+ vr = validate_query(q)
266
+ if vr.valid:
267
+ syntax_pass += 1
268
+ else:
269
+ syntax_fail += 1
270
+ all_passed = False
271
+
272
+ # Lint (errors only)
273
+ lr = lint_query(q)
274
+ errors = [f for f in lr.findings if f.severity == Severity.ERROR]
275
+ if errors:
276
+ lint_fail += 1
277
+ all_passed = False
278
+ rules = ", ".join(sorted({f.rule_id for f in errors}))
279
+ lint_failures.append(f" - **{label}**: {rules}")
280
+ else:
281
+ lint_pass += 1
282
+
283
+ lines.append("### Query Validation")
284
+ lines.append("")
285
+ lines.append(f"**{len(complete)}** complete queries found, **{len(snippets)}** snippets (skipped)")
286
+ lines.append("")
287
+ lines.append("| Check | Passed | Failed |")
288
+ lines.append("|-------|--------|--------|")
289
+ lines.append(f"| Syntax (balanced braces, structure) | {syntax_pass} | {syntax_fail} |")
290
+ lines.append(f"| Lint (ERROR-severity rules) | {lint_pass} | {lint_fail} |")
291
+ lines.append("")
292
+
293
+ if lint_failures:
294
+ lines.append("<details><summary>Lint failures</summary>")
295
+ lines.append("")
296
+ lines.extend(lint_failures)
297
+ lines.append("")
298
+ lines.append("</details>")
299
+ lines.append("")
300
+
301
+ # ── Section 3: Per-rule results ─────────────────────────────
302
+ rule_descriptions = {
303
+ "ENIGMA-001": ("ERROR", "LegalEntity.persons usage (usually empty)"),
304
+ "ENIGMA-002": ("ERROR", "Direct roles on LegalEntity"),
305
+ "ENIGMA-003": ("ERROR", "Missing pagination (first: N)"),
306
+ "ENIGMA-004": ("ERROR", "Missing entityType on searchInput"),
307
+ "ENIGMA-005": ("ERROR", "prompt with non-BRAND entity"),
308
+ "ENIGMA-006": ("ERROR", "Unfiltered cardTransactions"),
309
+ "ENIGMA-007": ("ERROR", "Deep traversal (>5 levels)"),
310
+ "ENIGMA-008": ("ERROR", "roles on Brand (invalid field)"),
311
+ "ENIGMA-009": ("ERROR", "Multi-field filter without AND/OR"),
312
+ "ENIGMA-010": ("ERROR", "Missing search limit"),
313
+ "ENIGMA-011": ("WARNING", "Unreliable HAS operator"),
314
+ "ENIGMA-012": ("WARNING", "Conditions on unsupported fields"),
315
+ "ENIGMA-013": ("WARNING", "Address-only search"),
316
+ "ENIGMA-014": ("WARNING", "rawQuantity instead of projectedQuantity"),
317
+ "ENIGMA-015": ("WARNING", "postalCode in return fields (use zip)"),
318
+ "ENIGMA-016": ("INFO", "Address ranking on PERSON search"),
319
+ "ENIGMA-017": ("INFO", "LegalEntity → Brand ~30% error rate"),
320
+ "ENIGMA-018": ("INFO", "Missing operation name"),
321
+ }
322
+
323
+ # Count how many queries triggered each rule
324
+ rule_trigger_count: dict[str, int] = {rid: 0 for rid in rule_descriptions}
325
+ for block in complete:
326
+ lr = lint_query(block["query"])
327
+ for f in lr.findings:
328
+ if f.rule_id in rule_trigger_count:
329
+ rule_trigger_count[f.rule_id] += 1
330
+
331
+ lines.append("### Lint Rules")
332
+ lines.append("")
333
+ lines.append(f"| Rule | Severity | Description | Result |")
334
+ lines.append("|------|----------|-------------|--------|")
335
+ for rule_id, (sev, desc) in rule_descriptions.items():
336
+ count = rule_trigger_count[rule_id]
337
+ if count == 0:
338
+ icon = "✅ Pass"
339
+ elif sev == "INFO":
340
+ icon = f"ℹ️ {count} note(s)"
341
+ elif sev == "WARNING":
342
+ icon = f"⚠️ {count} warning(s)"
343
+ else:
344
+ icon = f"❌ {count} error(s)"
345
+ lines.append(f"| {rule_id} | {sev} | {desc} | {icon} |")
346
+
347
+ lines.append("")
348
+
349
+ # ── Footer ────────────────────────────────────────────────────
350
+ total_checks = structural_pass + structural_fail + syntax_pass + syntax_fail + lint_pass + lint_fail
351
+ total_passed = structural_pass + syntax_pass + lint_pass
352
+ total_failed = structural_fail + syntax_fail + lint_fail
353
+
354
+ if all_passed:
355
+ lines.append(f"**Result: ✅ ALL CHECKS PASSED** ({total_passed}/{total_checks})")
356
+ else:
357
+ lines.append(f"**Result: ❌ {total_failed} CHECK(S) FAILED** ({total_passed}/{total_checks} passed)")
358
+
359
+ lines.append("")
360
+
361
+ return "\n".join(lines), all_passed
362
+
363
+
364
+ # ---------------------------------------------------------------------------
365
+ # CLI
366
+ # ---------------------------------------------------------------------------
367
+
368
+ def main():
369
+ if len(sys.argv) > 1:
370
+ skill_path = Path(sys.argv[1])
371
+ else:
372
+ skill_path = PLUGIN_ROOT / "skills" / "enigma-graphql" / "SKILL.md"
373
+
374
+ if not skill_path.exists():
375
+ print(f"ERROR: {skill_path} not found", file=sys.stderr)
376
+ sys.exit(1)
377
+
378
+ report, passed = generate_report(skill_path)
379
+ print(report)
380
+ sys.exit(0 if passed else 1)
381
+
382
+
383
+ if __name__ == "__main__":
384
+ main()