botversion-sdk 1.1.6__tar.gz → 1.1.7__tar.gz

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 (19) hide show
  1. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/PKG-INFO +1 -1
  2. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/interceptor.py +130 -3
  3. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/PKG-INFO +1 -1
  4. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/pyproject.toml +1 -1
  5. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/setup.py +1 -1
  6. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/__init__.py +0 -0
  7. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/__init__.py +0 -0
  8. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/detector.py +0 -0
  9. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/generator.py +0 -0
  10. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/init.py +0 -0
  11. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/prompts.py +0 -0
  12. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/cli/writer.py +0 -0
  13. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/client.py +0 -0
  14. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk/scanner.py +0 -0
  15. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/SOURCES.txt +0 -0
  16. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/dependency_links.txt +0 -0
  17. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/entry_points.txt +0 -0
  18. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/botversion_sdk.egg-info/top_level.txt +0 -0
  19. {botversion_sdk-1.1.6 → botversion_sdk-1.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: botversion-sdk
3
- Version: 1.1.6
3
+ Version: 1.1.7
4
4
  Summary: BotVersion AI Agent SDK for FastAPI, Flask, and Django
5
5
  Home-page: https://github.com/botversion/botversion-sdk-python
6
6
  Author: BotVersion
@@ -78,6 +78,19 @@ def build_body_structure(body):
78
78
  structure[key] = "array"
79
79
  elif val is None:
80
80
  structure[key] = "null"
81
+ elif isinstance(val, dict):
82
+ # Capture one level of nested properties so agent can reconstruct
83
+ # the object shape — e.g. group: { teamId: null, parentId: null }
84
+ nested_props = {}
85
+ for nk, nv in val.items():
86
+ if nv is None:
87
+ nested_props[nk] = {"type": "string"}
88
+ else:
89
+ nested_props[nk] = {"type": type(nv).__name__}
90
+ if nested_props:
91
+ structure[key] = {"type": "object", "properties": nested_props}
92
+ else:
93
+ structure[key] = "object"
81
94
  else:
82
95
  structure[key] = type(val).__name__
83
96
 
@@ -87,20 +100,76 @@ def build_body_structure(body):
87
100
  def body_structure_to_json_schema(body_structure):
88
101
  """
89
102
  Convert body structure dict to JSON Schema format.
103
+ Handles both simple types (strings) and nested object descriptors.
90
104
  """
91
105
  if not body_structure:
92
106
  return None
93
107
 
94
108
  properties = {}
95
- for key, type_name in body_structure.items():
96
- if type_name in ("[redacted]", "null"):
109
+ for key, type_or_obj in body_structure.items():
110
+ # Nested object captured with properties
111
+ if isinstance(type_or_obj, dict) and "type" in type_or_obj:
112
+ properties[key] = type_or_obj
113
+ # Simple type string
114
+ elif type_or_obj in ("[redacted]", "null"):
97
115
  properties[key] = {"type": "string"}
98
116
  else:
99
- properties[key] = {"type": type_name}
117
+ properties[key] = {"type": type_or_obj}
100
118
 
101
119
  return {"type": "object", "properties": properties}
102
120
 
103
121
 
122
+ def extract_trpc_get_input(path, query_string):
123
+ """
124
+ tRPC GET requests send their input as a URL-encoded JSON query parameter
125
+ called 'input'. This function extracts and parses it.
126
+ Only activates for tRPC paths with an input param and empty body.
127
+ Safe for all non-tRPC endpoints — returns None if not applicable.
128
+ """
129
+ if "/trpc/" not in path:
130
+ return None
131
+ if not query_string or "input=" not in query_string:
132
+ return None
133
+
134
+ try:
135
+ from urllib.parse import parse_qs, unquote
136
+ params = parse_qs(query_string)
137
+ input_param = params.get("input", [None])[0]
138
+ if not input_param:
139
+ return None
140
+
141
+ decoded = json.loads(unquote(input_param))
142
+ if not isinstance(decoded, dict):
143
+ return None
144
+
145
+ keys = list(decoded.keys())
146
+
147
+ # Format 1 — batched: { "0": { "json": {...} }, "1": { "json": {...} } }
148
+ is_batch = len(keys) > 0 and all(k.isdigit() for k in keys)
149
+ if is_batch:
150
+ merged = {}
151
+ for k in keys:
152
+ entry = decoded[k]
153
+ if isinstance(entry, dict):
154
+ # Unwrap { json: {...} } envelope if present
155
+ unwrapped = entry.get("json", entry) if isinstance(entry.get("json"), dict) else entry
156
+ merged.update(unwrapped)
157
+ return merged if merged else None
158
+
159
+ # Format 2 — single with superjson: { "json": {...}, "meta": {...} }
160
+ if "json" in decoded and isinstance(decoded["json"], dict):
161
+ return decoded["json"]
162
+
163
+ # Format 3 — already unwrapped: { "group": {...}, "limit": 10 }
164
+ if "meta" not in decoded and "json" not in decoded:
165
+ return decoded
166
+
167
+ except Exception:
168
+ pass
169
+
170
+ return None
171
+
172
+
104
173
  def report_endpoint(client, method, path, body_structure, options):
105
174
  """
106
175
  Report a newly discovered endpoint to the platform.
@@ -159,6 +228,12 @@ def attach_fastapi_interceptor(app, client, options):
159
228
  request._receive = receive
160
229
  body_data = _json.loads(body_bytes) if body_bytes else None
161
230
  body_structure = build_body_structure(body_data)
231
+ # tRPC GET: input is in URL query param, not body
232
+ if not body_structure:
233
+ query_string = request.url.query
234
+ trpc_body = extract_trpc_get_input(path, query_string)
235
+ if trpc_body:
236
+ body_structure = build_body_structure(trpc_body)
162
237
  except Exception:
163
238
  body_structure = None
164
239
 
@@ -191,6 +266,11 @@ def attach_flask_interceptor(app, client, options):
191
266
 
192
267
  try:
193
268
  body_structure = build_body_structure(flask_request.get_json(silent=True))
269
+ # tRPC GET: input is in URL query param, not body
270
+ if not body_structure:
271
+ trpc_body = extract_trpc_get_input(path, flask_request.query_string.decode("utf-8"))
272
+ if trpc_body:
273
+ body_structure = build_body_structure(trpc_body)
194
274
  except Exception:
195
275
  body_structure = None
196
276
 
@@ -222,6 +302,11 @@ class BotVersionDjangoMiddleware:
222
302
  try:
223
303
  body_data = json.loads(request.body) if request.body else None
224
304
  body_structure = build_body_structure(body_data)
305
+ # tRPC GET: input is in URL query param, not body
306
+ if not body_structure:
307
+ trpc_body = extract_trpc_get_input(path, request.META.get("QUERY_STRING", ""))
308
+ if trpc_body:
309
+ body_structure = build_body_structure(trpc_body)
225
310
  except Exception:
226
311
  body_structure = None
227
312
 
@@ -281,6 +366,12 @@ def attach_starlette_interceptor(app, client, options):
281
366
  body_bytes = await request.body()
282
367
  body_data = _json.loads(body_bytes) if body_bytes else None
283
368
  body_structure = build_body_structure(body_data)
369
+ # tRPC GET: input is in URL query param, not body
370
+ if not body_structure:
371
+ query_string = request.url.query
372
+ trpc_body = extract_trpc_get_input(path, query_string)
373
+ if trpc_body:
374
+ body_structure = build_body_structure(trpc_body)
284
375
  except Exception:
285
376
  body_structure = None
286
377
 
@@ -312,6 +403,11 @@ def attach_sanic_interceptor(app, client, options):
312
403
  try:
313
404
  body_data = request.json if request.body else None
314
405
  body_structure = build_body_structure(body_data)
406
+ # tRPC GET: input is in URL query param, not body
407
+ if not body_structure:
408
+ trpc_body = extract_trpc_get_input(path, request.query_string)
409
+ if trpc_body:
410
+ body_structure = build_body_structure(trpc_body)
315
411
  except Exception:
316
412
  body_structure = None
317
413
 
@@ -341,6 +437,11 @@ def attach_falcon_interceptor(app, client, options):
341
437
  body_bytes = req.bounded_stream.read()
342
438
  body_data = _json.loads(body_bytes) if body_bytes else None
343
439
  body_structure = build_body_structure(body_data)
440
+ # tRPC GET: input is in URL query param, not body
441
+ if not body_structure:
442
+ trpc_body = extract_trpc_get_input(path, req.query_string)
443
+ if trpc_body:
444
+ body_structure = build_body_structure(trpc_body)
344
445
  # Put body back so the actual handler can still read it
345
446
  import io
346
447
  req.bounded_stream = io.BytesIO(body_bytes)
@@ -373,6 +474,11 @@ def attach_bottle_interceptor(app, client, options):
373
474
  try:
374
475
  body_data = bottle_request.json
375
476
  body_structure = build_body_structure(body_data)
477
+ # tRPC GET: input is in URL query param, not body
478
+ if not body_structure:
479
+ trpc_body = extract_trpc_get_input(path, bottle_request.query_string)
480
+ if trpc_body:
481
+ body_structure = build_body_structure(trpc_body)
376
482
  except Exception:
377
483
  body_structure = None
378
484
 
@@ -405,6 +511,11 @@ def attach_aiohttp_interceptor(app, client, options):
405
511
  body_bytes = await request.read()
406
512
  body_data = _json.loads(body_bytes) if body_bytes else None
407
513
  body_structure = build_body_structure(body_data)
514
+ # tRPC GET: input is in URL query param, not body
515
+ if not body_structure:
516
+ trpc_body = extract_trpc_get_input(path, request.rel_url.query_string)
517
+ if trpc_body:
518
+ body_structure = build_body_structure(trpc_body)
408
519
  except Exception:
409
520
  body_structure = None
410
521
 
@@ -450,6 +561,11 @@ def attach_tornado_interceptor(app, client, options):
450
561
  body_bytes = self.request.body
451
562
  body_data = _json.loads(body_bytes) if body_bytes else None
452
563
  body_structure = build_body_structure(body_data)
564
+ # tRPC GET: input is in URL query param, not body
565
+ if not body_structure:
566
+ trpc_body = extract_trpc_get_input(path, self.request.query)
567
+ if trpc_body:
568
+ body_structure = build_body_structure(trpc_body)
453
569
  except Exception:
454
570
  body_structure = None
455
571
 
@@ -487,6 +603,11 @@ def botversion_pyramid_tween_factory(handler, registry):
487
603
  try:
488
604
  body_data = request.json_body if request.content_length else None
489
605
  body_structure = build_body_structure(body_data)
606
+ # tRPC GET: input is in URL query param, not body
607
+ if not body_structure:
608
+ trpc_body = extract_trpc_get_input(path, request.query_string)
609
+ if trpc_body:
610
+ body_structure = build_body_structure(trpc_body)
490
611
  except Exception:
491
612
  body_structure = None
492
613
 
@@ -533,6 +654,12 @@ def attach_cherrypy_interceptor(app, client, options):
533
654
  body_bytes = request.body.read() if request.body else None
534
655
  body_data = _json.loads(body_bytes) if body_bytes else None
535
656
  body_structure = build_body_structure(body_data)
657
+ # tRPC GET: input is in URL query param, not body
658
+ if not body_structure:
659
+ query_string = request.query_string if hasattr(request, "query_string") else ""
660
+ trpc_body = extract_trpc_get_input(path, query_string)
661
+ if trpc_body:
662
+ body_structure = build_body_structure(trpc_body)
536
663
  # Put body back so actual handler can still read it
537
664
  import io
538
665
  request.body = io.BytesIO(body_bytes or b"")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: botversion-sdk
3
- Version: 1.1.6
3
+ Version: 1.1.7
4
4
  Summary: BotVersion AI Agent SDK for FastAPI, Flask, and Django
5
5
  Home-page: https://github.com/botversion/botversion-sdk-python
6
6
  Author: BotVersion
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "botversion-sdk"
7
- version = "1.1.6"
7
+ version = "1.1.7"
8
8
  description = "BotVersion AI Agent SDK for FastAPI, Flask, and Django"
9
9
  authors = [{name = "Saurav Dhakal", email = "sauravdhakal828@gmail.com"}]
10
10
  requires-python = ">=3.8"
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="botversion-sdk",
5
- version="1.1.6",
5
+ version="1.1.7",
6
6
  description="BotVersion SDK — automatically discover and register your API endpoints",
7
7
  long_description=open("README.md").read() if __import__("os").path.exists("README.md") else "",
8
8
  long_description_content_type="text/markdown",
File without changes