hubric 0.5.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 (64) hide show
  1. hubric/__init__.py +6 -0
  2. hubric/__main__.py +5 -0
  3. hubric/auth/__init__.py +8 -0
  4. hubric/auth/classifier.py +187 -0
  5. hubric/auth/interviewer.py +497 -0
  6. hubric/auth/resolver.py +160 -0
  7. hubric/auth/schemes.py +188 -0
  8. hubric/auth/store.py +292 -0
  9. hubric/auth/verifier.py +325 -0
  10. hubric/cli/__init__.py +1 -0
  11. hubric/cli/commands/ci.py +53 -0
  12. hubric/cli/commands/forecast.py +241 -0
  13. hubric/cli/commands/inspect.py +76 -0
  14. hubric/cli/commands/payload.py +127 -0
  15. hubric/cli/commands/run.py +706 -0
  16. hubric/cli/commands/settings.py +44 -0
  17. hubric/cli/commands/trace.py +195 -0
  18. hubric/cli/helpers.py +527 -0
  19. hubric/cli/main.py +116 -0
  20. hubric/cli/tracer.py +109 -0
  21. hubric/config/__init__.py +5 -0
  22. hubric/config/loader.py +108 -0
  23. hubric/core/__init__.py +1 -0
  24. hubric/core/assertions.py +152 -0
  25. hubric/core/env.py +30 -0
  26. hubric/core/llm.py +545 -0
  27. hubric/core/schema.py +964 -0
  28. hubric/core/security.py +451 -0
  29. hubric/core/synthesizer.py +551 -0
  30. hubric/engine/__init__.py +1 -0
  31. hubric/engine/aggregator.py +211 -0
  32. hubric/engine/auth_pool.py +126 -0
  33. hubric/engine/dispatcher.py +69 -0
  34. hubric/engine/executor.py +792 -0
  35. hubric/engine/journey.py +806 -0
  36. hubric/engine/rate_limit.py +56 -0
  37. hubric/engine/registry.py +351 -0
  38. hubric/engine/runner.py +5 -0
  39. hubric/engine/scheduler.py +645 -0
  40. hubric/engine/types.py +55 -0
  41. hubric/engine/utils.py +214 -0
  42. hubric/forecast/__init__.py +0 -0
  43. hubric/forecast/analyzer.py +350 -0
  44. hubric/forecast/reporter.py +1234 -0
  45. hubric/report/__init__.py +1 -0
  46. hubric/report/diagnostics.py +122 -0
  47. hubric/report/export.py +14 -0
  48. hubric/report/export_html.py +158 -0
  49. hubric/report/export_json.py +23 -0
  50. hubric/report/html_assets.py +154 -0
  51. hubric/report/html_helpers.py +135 -0
  52. hubric/report/html_sections.py +429 -0
  53. hubric/report/junit.py +100 -0
  54. hubric/report/metrics.py +435 -0
  55. hubric/report/reporter.py +8 -0
  56. hubric/report/scoring.py +288 -0
  57. hubric/report/terminal.py +506 -0
  58. hubric/settings.py +355 -0
  59. hubric-0.5.0.dist-info/METADATA +395 -0
  60. hubric-0.5.0.dist-info/RECORD +64 -0
  61. hubric-0.5.0.dist-info/WHEEL +5 -0
  62. hubric-0.5.0.dist-info/entry_points.txt +3 -0
  63. hubric-0.5.0.dist-info/licenses/LICENSE +21 -0
  64. hubric-0.5.0.dist-info/top_level.txt +1 -0
hubric/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """
2
+ Hubric — Zero-Config, LLM-Powered API Load Tester.
3
+ """
4
+
5
+ __version__ = "0.5.0"
6
+
hubric/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+ from hubric.cli.main import main
3
+
4
+ if __name__ == "__main__":
5
+ sys.exit(main())
@@ -0,0 +1,8 @@
1
+ from .store import load_auth, save_auth, delete_credentials, auth_file_exists
2
+ from .interviewer import run_auth_interview
3
+ from .resolver import resolve_headers
4
+
5
+ __all__ = [
6
+ "load_auth", "save_auth", "delete_credentials", "auth_file_exists",
7
+ "run_auth_interview", "resolve_headers",
8
+ ]
@@ -0,0 +1,187 @@
1
+ """
2
+ Auth classification rules and pattern detection.
3
+
4
+ Categorizes endpoints into:
5
+ - token_source: login, register, signup (creates tokens; tested with payloads)
6
+ - untestable: email verification, OAuth callbacks, password resets, OTP/MFA
7
+ - external_dep: Stripe, Resend, Twilio, etc.
8
+ """
9
+
10
+ import re
11
+
12
+ # Routes that generate tokens from public credentials — should never require a token to be tested
13
+ TOKEN_SOURCE_SIGNALS = (
14
+ "/login", "/register", "/signup", "/sign-up", "/sign_up",
15
+ "/token", "/auth/token", "/refresh", "/auth/refresh",
16
+ "/signin", "/sign-in", "/sign_in", "/api/login", "/api/auth/login",
17
+ "/api/authenticate", "/authenticate", "/oauth/token", "/oauth2/token",
18
+ "/users/login", "/user/login", "/session"
19
+ )
20
+
21
+ # Routes that depend on out-of-band flows — can never be load tested
22
+ UNTESTABLE_PATTERNS = [
23
+ # Email / phone verification & confirmation & activation
24
+ r"verify.*(email|phone|account|user|mobile)",
25
+ r"(email|phone|account|user|mobile).*verify",
26
+ r"confirm.*(email|phone|account|user)",
27
+ r"(email|phone|account|user).*confirm",
28
+ r"activate.*(account|user)",
29
+ r"(account|user).*activate",
30
+ r"resend.*(verif|email|code|otp|token|activation)",
31
+ r"(send|request).*(verif|code|otp|activation)",
32
+
33
+ # Password recovery & reset
34
+ r"reset.*password",
35
+ r"password.*reset",
36
+ r"forgot.*password",
37
+ r"password.*forgot",
38
+ r"recover.*password",
39
+ r"password.*recover",
40
+ r"change.*password.*token",
41
+
42
+ # Two-factor / MFA / Passkey / OTP
43
+ r"\bmfa\b", r"\b2fa\b", r"sms.*code",
44
+ r"\btotp\b", r"\bwebauthn\b", r"\bfido\b",
45
+ r"magic.?link",
46
+ r"send.?otp",
47
+
48
+ # Outbound notifications / invites / dispatches (trigger external email/SMS without testable state)
49
+ r"resend",
50
+ r"\binvite(s|d)?\b",
51
+ r"\binvitations?\b",
52
+ r"request.*(otp|code|token|verif|link)",
53
+ r"dispatch.*(email|sms|notification)",
54
+
55
+ # OAuth / SAML / Webhook callbacks
56
+ r"oauth.*callback",
57
+ r"auth0.*callback",
58
+ r"saml.*acs",
59
+ r"webhook.*callback",
60
+ r"/callbacks?(/|$)",
61
+ ]
62
+
63
+ # Patterns specifically identifying outbound dispatch triggers
64
+ OUTBOUND_DISPATCH_PATTERNS = [
65
+ r"resend",
66
+ r"send.?otp",
67
+ r"magic.?link",
68
+ r"\binvite(s|d)?\b",
69
+ r"\binvitations?\b",
70
+ r"request.*(otp|code|token|verif|link)",
71
+ r"dispatch.*(email|sms|notification)",
72
+ ]
73
+
74
+ def _is_outbound_dispatch(text: str) -> bool:
75
+ """Returns True if text indicates an outbound notification or invitation route."""
76
+ text_lower = text.lower()
77
+ return any(re.search(p, text_lower) for p in OUTBOUND_DISPATCH_PATTERNS)
78
+
79
+
80
+ # Known external service signals in route, summary, description, body fields
81
+ EXTERNAL_SERVICE_PATTERNS = [
82
+ r"stripe", r"paystack", r"flutterwave", r"paypal",
83
+ r"resend", r"sendgrid", r"mailgun", r"twilio",
84
+ r"webhook", r"callback",
85
+ r"auth0", r"cognito", r"clerk", r"supabase", r"firebase", r"okta", r"kinde",
86
+ r"square", r"braintree", r"razorpay", r"paddle", r"lemonsqueezy",
87
+ r"aws-sns", r"plaid", r"hubspot", r"salesforce"
88
+ ]
89
+
90
+ # Parameter and body field names that indicate out-of-band flows (email codes, OTPs, magic links)
91
+ OOB_BODY_SIGNALS = {
92
+ "oob_code", "oobCode", "otp", "otp_code", "otpCode",
93
+ "verification_code", "verificationCode", "verify_code", "verifyCode",
94
+ "activation_token", "activationToken", "activation_code", "activationCode",
95
+ "reset_token", "resetToken", "reset_code", "resetCode",
96
+ "confirm_token", "confirmToken", "confirmation_code", "confirmationCode",
97
+ "magic_link_token", "magicLinkToken",
98
+ "email_token", "emailToken", "email_code", "emailCode",
99
+ "sms_code", "smsCode", "mfa_token", "mfaToken", "mfa_code", "mfaCode",
100
+ "totp", "totp_code", "totpCode", "two_factor_code", "two_factor_token",
101
+ "passcode"
102
+ }
103
+
104
+
105
+ def _is_token_source(ep: dict) -> bool:
106
+ """
107
+ Returns True if this endpoint generates tokens rather than consuming them.
108
+ These need valid request payloads, not Authorization headers.
109
+ Rubric tests them as public endpoints with synthesized bodies.
110
+ """
111
+ route = ep.get("route", "").lower()
112
+ return any(signal in route for signal in TOKEN_SOURCE_SIGNALS)
113
+
114
+
115
+ def _is_refresh_endpoint(ep: dict) -> bool:
116
+ """Returns True if this endpoint is a token refresh endpoint."""
117
+ route = ep.get("route", "").lower()
118
+ summary = ep.get("summary", "").lower()
119
+ description = ep.get("description", "").lower()
120
+ text = f"{route} {summary} {description}"
121
+ return bool(
122
+ re.search(r"\b(auth/)?(token/)?refresh\b", text)
123
+ or "/refresh" in route
124
+ or "refresh token" in text
125
+ )
126
+
127
+
128
+ def _detect_untestable(ep: dict) -> bool:
129
+ """Returns True if the endpoint requires an out-of-band flow.
130
+
131
+ Detection strategy:
132
+ 1. Route/summary/description regex matching against UNTESTABLE_PATTERNS
133
+ 2. Parameter inspection — if ANY body, query, or path parameter
134
+ matches OOB_BODY_SIGNALS, the endpoint requires an external code/token
135
+ that Rubric cannot synthesize (e.g. resetToken, otp, email_code).
136
+ 3. Parameter inspection for generic 'token' or 'code' on verification/reset routes.
137
+ """
138
+ text = " ".join([
139
+ ep.get("route", ""),
140
+ ep.get("summary", ""),
141
+ ep.get("description", ""),
142
+ ]).lower()
143
+ if any(re.search(p, text) for p in UNTESTABLE_PATTERNS) or _is_outbound_dispatch(text):
144
+ return True
145
+
146
+ # Collect parameter names across body, query, and path params
147
+ param_names = set()
148
+ for p in ep.get("query_params", []) + ep.get("path_params", []):
149
+ param_names.add(p.get("name", "").lower())
150
+
151
+ body_schema = ep.get("body_schema") or {}
152
+ properties = body_schema.get("properties") or {}
153
+ param_names.update(k.lower() for k in properties.keys())
154
+
155
+ oob_lower = {k.lower() for k in OOB_BODY_SIGNALS}
156
+ if param_names.intersection(oob_lower):
157
+ return True
158
+
159
+ # Generic token/code on verification/reset endpoints
160
+ if ("token" in param_names or "code" in param_names) and any(
161
+ kw in text for kw in ("verify", "confirm", "reset", "activate", "unlock")
162
+ ):
163
+ return True
164
+
165
+ return False
166
+
167
+
168
+ def _detect_external(ep: dict) -> str | None:
169
+ """
170
+ Returns a short label if the endpoint looks like an external service dep.
171
+ Checks route, summary, description, tags, and body field names.
172
+ """
173
+ targets = [
174
+ ep.get("route", ""),
175
+ ep.get("summary", ""),
176
+ ep.get("description", ""),
177
+ " ".join(ep.get("tags", [])),
178
+ ]
179
+ body = ep.get("body_schema") or {}
180
+ targets += list((body.get("properties") or {}).keys())
181
+ text = " ".join(targets).lower()
182
+
183
+ for pattern in EXTERNAL_SERVICE_PATTERNS:
184
+ if re.search(pattern, text):
185
+ match = re.search(pattern, text)
186
+ return match.group(0) if match else "external service"
187
+ return None