trodo-python 2.16.0__py3-none-any.whl → 2.17.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.
trodo/__init__.py CHANGED
@@ -119,9 +119,14 @@ __all__ = [
119
119
  "TemplateError",
120
120
  "ManagedPrompt",
121
121
  "PromptSummary",
122
- # Datasets & experiments ingest
122
+ # Datasets & experiments
123
123
  "append_dataset",
124
124
  "ingest_experiment",
125
+ "create_experiment",
126
+ "run_experiment",
127
+ "get_experiment",
128
+ "wait_experiment",
129
+ "compare_experiments",
125
130
  ]
126
131
 
127
132
  # ============================================================================
@@ -405,6 +410,54 @@ def ingest_experiment(
405
410
  )
406
411
 
407
412
 
413
+ def create_experiment(
414
+ *,
415
+ dataset: Optional[str] = None,
416
+ dataset_id: Optional[str] = None,
417
+ models: List[Dict[str, Any]],
418
+ **kwargs: Any,
419
+ ) -> Dict[str, Any]:
420
+ """Create an experiment (immutable refs, status pending)."""
421
+ return _get_client().experiments.create(
422
+ dataset=dataset, dataset_id=dataset_id, models=models, **kwargs
423
+ )
424
+
425
+
426
+ def run_experiment(
427
+ *,
428
+ dataset: Optional[str] = None,
429
+ dataset_id: Optional[str] = None,
430
+ models: List[Dict[str, Any]],
431
+ **kwargs: Any,
432
+ ) -> Dict[str, Any]:
433
+ """Create and run an experiment synchronously."""
434
+ return _get_client().experiments.run(
435
+ dataset=dataset, dataset_id=dataset_id, models=models, **kwargs
436
+ )
437
+
438
+
439
+ def get_experiment(experiment_id: str) -> Dict[str, Any]:
440
+ """Fetch an experiment by id."""
441
+ return _get_client().experiments.get(experiment_id)
442
+
443
+
444
+ def wait_experiment(
445
+ experiment_id: str,
446
+ *,
447
+ interval_ms: int = 2500,
448
+ timeout_ms: int = 600_000,
449
+ ) -> Dict[str, Any]:
450
+ """Poll until the experiment completes or fails."""
451
+ return _get_client().experiments.wait(
452
+ experiment_id, interval_ms=interval_ms, timeout_ms=timeout_ms
453
+ )
454
+
455
+
456
+ def compare_experiments(a_id: str, b_id: str, *, strict: bool = False) -> Dict[str, Any]:
457
+ """Compare two experiments side by side."""
458
+ return _get_client().experiments.compare(a_id, b_id, strict=strict)
459
+
460
+
408
461
  def enable_auto_events() -> None:
409
462
  _get_client().enable_auto_events()
410
463
 
trodo/api/endpoints.py CHANGED
@@ -23,6 +23,10 @@ RUNS_INGEST = "/api/sdk/runs/ingest"
23
23
  RUNS_START = "/api/sdk/runs/start"
24
24
  RUNS_BASE = "/api/sdk/runs" # /runs/{run_id}/end, /spans, /feedback
25
25
  OTLP_TRACES = "/api/sdk/otel/v1/traces"
26
- # Datasets & experiments ingest
26
+ # Datasets & experiments
27
27
  DATASETS_BASE = "/api/sdk/datasets" # /datasets/{ref}/items
28
+ EXPERIMENTS = "/api/sdk/experiments"
28
29
  EXPERIMENTS_INGEST = "/api/sdk/experiments/ingest"
30
+ EXPERIMENTS_CREATE = "/api/sdk/experiments/create"
31
+ EXPERIMENTS_RUN = "/api/sdk/experiments/run"
32
+ EXPERIMENTS_COMPARE = "/api/sdk/experiments/compare"
trodo/api/http_client.py CHANGED
@@ -143,6 +143,28 @@ class HttpClient:
143
143
  def ingest_experiment(self, body: Dict[str, Any]) -> ApiResult:
144
144
  return self._request("/api/sdk/experiments/ingest", body)
145
145
 
146
+ def create_experiment(self, body: Dict[str, Any]) -> ApiResult:
147
+ return self._request("/api/sdk/experiments/create", body)
148
+
149
+ def run_experiment(self, body: Dict[str, Any]) -> ApiResult:
150
+ return self._request("/api/sdk/experiments/run", body)
151
+
152
+ def run_experiment_by_id(self, experiment_id: str) -> ApiResult:
153
+ from urllib.parse import quote
154
+ return self._request(f"/api/sdk/experiments/{quote(str(experiment_id), safe='')}/run", {})
155
+
156
+ def get_experiment(self, experiment_id: str) -> ApiResult:
157
+ from urllib.parse import quote
158
+ return self._get(f"/api/sdk/experiments/{quote(str(experiment_id), safe='')}")
159
+
160
+ def compare_experiments(
161
+ self, a_id: str, b_id: str, strict: bool = False
162
+ ) -> ApiResult:
163
+ params: Dict[str, Any] = {"a": a_id, "b": b_id}
164
+ if strict:
165
+ params["strict"] = "true"
166
+ return self._get("/api/sdk/experiments/compare", params)
167
+
146
168
  def post_track(self, session_data: Dict[str, Any]) -> ApiResult:
147
169
  return self._request("/api/sdk/track", {"sessionData": session_data})
148
170
 
trodo/client.py CHANGED
@@ -137,7 +137,7 @@ class TrodoClient:
137
137
 
138
138
  @property
139
139
  def experiments(self):
140
- """Ingest experiment result batches for server-side grading."""
140
+ """Managed experiment runs + external ingest for server-side grading."""
141
141
  if self._experiments is None:
142
142
  from .managers.experiment_manager import ExperimentManager
143
143
  self._experiments = ExperimentManager(self._http)
@@ -1,26 +1,25 @@
1
- """Experiment ingestrecord a batch of model outputs against a dataset.
1
+ """Experiment managermanaged runs and external ingest.
2
2
 
3
- An experiment takes a dataset (the fixed inputs) and a set of *outputs* your
4
- model produced for each item, then grades them server-side using the judge
5
- and/or evaluators you name. You run the model yourself and hand Trodo the
6
- results; Trodo scores and stores them so runs are comparable over time.
3
+ Managed experiments (prompt + model over a dataset snapshot) use
4
+ ``create``, ``run``, ``get``, ``wait``, and ``compare``.
5
+ External agent outputs use ``ingest``.
7
6
 
8
- ``outputs`` are matched to dataset items by ``item_position`` (0-based),
9
- carrying the produced ``output`` and optional per-row context (``query``,
10
- ``context``), cost/latency telemetry, and a per-row ``expected_output`` override.
11
-
12
- Mirrors ``sdks/trodo-node-sdk/src/managers/ExperimentManager.ts`` in shape.
7
+ Mirrors ``sdks/trodo-node-sdk/src/managers/ExperimentManager.ts``.
13
8
  """
14
9
 
15
10
  from __future__ import annotations
16
11
 
17
- from typing import Any, Dict, List, Optional
12
+ import time
13
+ from typing import Any, Dict, List, Optional, Union
18
14
 
19
15
  __all__ = ["ExperimentManager"]
20
16
 
21
- # Optional per-output fields, in wire order. ``item_position`` + ``output`` are
22
- # always required.
17
+ _TERMINAL = frozenset({"completed", "failed"})
18
+
19
+ # Optional per-output fields for ingest
23
20
  _OUTPUT_OPTIONAL = (
21
+ "item_position",
22
+ "dataset_item_id",
24
23
  "expected_output",
25
24
  "query",
26
25
  "context",
@@ -32,22 +31,79 @@ _OUTPUT_OPTIONAL = (
32
31
  def _clean_output(output: Dict[str, Any]) -> Dict[str, Any]:
33
32
  if not isinstance(output, dict):
34
33
  raise ValueError("trodo: each experiment output must be a dict")
35
- if output.get("item_position") is None:
36
- raise ValueError("trodo: each output requires an 'item_position' (int)")
37
34
  if output.get("output") is None:
38
35
  raise ValueError("trodo: each output requires an 'output' (str)")
39
- out: Dict[str, Any] = {
40
- "item_position": output["item_position"],
41
- "output": output["output"],
42
- }
36
+ if output.get("item_position") is None and output.get("dataset_item_id") is None:
37
+ raise ValueError(
38
+ "trodo: each output requires 'item_position' and/or 'dataset_item_id'"
39
+ )
40
+ out: Dict[str, Any] = {"output": output["output"]}
43
41
  for key in _OUTPUT_OPTIONAL:
44
42
  if output.get(key) is not None:
45
43
  out[key] = output[key]
46
44
  return out
47
45
 
48
46
 
47
+ def _unwrap_experiment(res: Any) -> Dict[str, Any]:
48
+ if isinstance(res, dict) and "experiment" in res:
49
+ return res["experiment"]
50
+ return res if isinstance(res, dict) else {}
51
+
52
+
53
+ def _build_managed_body(
54
+ *,
55
+ dataset: Optional[str] = None,
56
+ dataset_id: Optional[str] = None,
57
+ dataset_version_no: Optional[int] = None,
58
+ name: Optional[str] = None,
59
+ prompt_ref: Optional[Dict[str, Any]] = None,
60
+ prompt_version_id: Optional[str] = None,
61
+ prompt_id: Optional[str] = None,
62
+ use_prompt: Optional[bool] = None,
63
+ models: Optional[List[Dict[str, Any]]] = None,
64
+ scorer_ids: Optional[List[Union[str, int]]] = None,
65
+ evaluator_ids: Optional[List[Union[str, int]]] = None,
66
+ deep_eval: Optional[bool] = None,
67
+ stability_runs: Optional[int] = None,
68
+ row_limit: Optional[int] = None,
69
+ concurrency: Optional[int] = None,
70
+ metadata: Optional[Dict[str, Any]] = None,
71
+ ) -> Dict[str, Any]:
72
+ body: Dict[str, Any] = {"models": models or []}
73
+ ds = dataset_id or dataset
74
+ if ds:
75
+ body["dataset_id"] = ds
76
+ if dataset_version_no is not None:
77
+ body["dataset_version_no"] = dataset_version_no
78
+ if name is not None:
79
+ body["name"] = name
80
+ if prompt_ref is not None:
81
+ body["prompt_ref"] = prompt_ref
82
+ if prompt_version_id is not None:
83
+ body["prompt_version_id"] = prompt_version_id
84
+ if prompt_id is not None:
85
+ body["prompt_id"] = prompt_id
86
+ if use_prompt is False:
87
+ body["use_prompt"] = False
88
+ ids = scorer_ids if scorer_ids is not None else evaluator_ids
89
+ if ids is not None:
90
+ body["scorer_ids"] = ids
91
+ body["evaluator_ids"] = ids
92
+ if deep_eval is not None:
93
+ body["deep_eval"] = deep_eval
94
+ if stability_runs is not None:
95
+ body["stability_runs"] = stability_runs
96
+ if row_limit is not None:
97
+ body["row_limit"] = row_limit
98
+ if concurrency is not None:
99
+ body["concurrency"] = concurrency
100
+ if metadata is not None:
101
+ body["metadata"] = metadata
102
+ return body
103
+
104
+
49
105
  class ExperimentManager:
50
- """Ingest experiment result batches for server-side grading."""
106
+ """Run and inspect Trodo experiments programmatically."""
51
107
 
52
108
  def __init__(self, http_client: Any) -> None:
53
109
  self._http = http_client
@@ -62,23 +118,7 @@ class ExperimentManager:
62
118
  evaluator_ids: Optional[List[str]] = None,
63
119
  dataset_version_no: Optional[int] = None,
64
120
  ) -> Dict[str, Any]:
65
- """Ingest a batch of *outputs* for *dataset* and return the experiment.
66
-
67
- *dataset* is a dataset name or UUID. *outputs* is a list of dicts shaped
68
- ``{"item_position": int, "output": str, "expected_output"?, "query"?,
69
- "context"?, "cost_usd"?, "latency_ms"?}``. ``None`` optional fields are
70
- dropped from the wire payload.
71
-
72
- Grading is configured server-side: pass ``judge`` (``{"credential_id",
73
- "provider", "model"}``) for an LLM judge and/or ``evaluator_ids`` for
74
- named evaluators. ``dataset_version_no`` pins the dataset version graded
75
- against.
76
-
77
- Returns the experiment dict from ``{"experiment": {...}}``.
78
-
79
- :raises ValueError: if *dataset* is empty, *outputs* is empty, or an
80
- output is missing ``item_position``/``output``.
81
- """
121
+ """Ingest externally-produced outputs and grade them (external task path)."""
82
122
  if not dataset:
83
123
  raise ValueError("trodo: ingest_experiment(dataset) requires a dataset")
84
124
  if not outputs:
@@ -99,7 +139,148 @@ class ExperimentManager:
99
139
  if dataset_version_no is not None:
100
140
  body["dataset_version_no"] = dataset_version_no
101
141
 
102
- res = self._http.ingest_experiment(body)
103
- if isinstance(res, dict) and "experiment" in res:
104
- return res["experiment"]
105
- return res
142
+ return _unwrap_experiment(self._http.ingest_experiment(body))
143
+
144
+ def create(
145
+ self,
146
+ *,
147
+ dataset: Optional[str] = None,
148
+ dataset_id: Optional[str] = None,
149
+ models: List[Dict[str, Any]],
150
+ dataset_version_no: Optional[int] = None,
151
+ name: Optional[str] = None,
152
+ prompt_ref: Optional[Dict[str, Any]] = None,
153
+ prompt_version_id: Optional[str] = None,
154
+ prompt_id: Optional[str] = None,
155
+ use_prompt: Optional[bool] = None,
156
+ scorer_ids: Optional[List[Union[str, int]]] = None,
157
+ evaluator_ids: Optional[List[Union[str, int]]] = None,
158
+ deep_eval: Optional[bool] = None,
159
+ stability_runs: Optional[int] = None,
160
+ row_limit: Optional[int] = None,
161
+ concurrency: Optional[int] = None,
162
+ metadata: Optional[Dict[str, Any]] = None,
163
+ ) -> Dict[str, Any]:
164
+ """Create an experiment (immutable refs, status pending)."""
165
+ self._validate_run_params(dataset, dataset_id, models)
166
+ body = _build_managed_body(
167
+ dataset=dataset,
168
+ dataset_id=dataset_id,
169
+ dataset_version_no=dataset_version_no,
170
+ name=name,
171
+ prompt_ref=prompt_ref,
172
+ prompt_version_id=prompt_version_id,
173
+ prompt_id=prompt_id,
174
+ use_prompt=use_prompt,
175
+ models=models,
176
+ scorer_ids=scorer_ids,
177
+ evaluator_ids=evaluator_ids,
178
+ deep_eval=deep_eval,
179
+ stability_runs=stability_runs,
180
+ row_limit=row_limit,
181
+ concurrency=concurrency,
182
+ metadata=metadata,
183
+ )
184
+ return _unwrap_experiment(self._http.create_experiment(body))
185
+
186
+ def run(
187
+ self,
188
+ *,
189
+ dataset: Optional[str] = None,
190
+ dataset_id: Optional[str] = None,
191
+ models: List[Dict[str, Any]],
192
+ dataset_version_no: Optional[int] = None,
193
+ name: Optional[str] = None,
194
+ prompt_ref: Optional[Dict[str, Any]] = None,
195
+ prompt_version_id: Optional[str] = None,
196
+ prompt_id: Optional[str] = None,
197
+ use_prompt: Optional[bool] = None,
198
+ scorer_ids: Optional[List[Union[str, int]]] = None,
199
+ evaluator_ids: Optional[List[Union[str, int]]] = None,
200
+ deep_eval: Optional[bool] = None,
201
+ stability_runs: Optional[int] = None,
202
+ row_limit: Optional[int] = None,
203
+ concurrency: Optional[int] = None,
204
+ metadata: Optional[Dict[str, Any]] = None,
205
+ ) -> Dict[str, Any]:
206
+ """Create and execute an experiment synchronously."""
207
+ self._validate_run_params(dataset, dataset_id, models)
208
+ body = _build_managed_body(
209
+ dataset=dataset,
210
+ dataset_id=dataset_id,
211
+ dataset_version_no=dataset_version_no,
212
+ name=name,
213
+ prompt_ref=prompt_ref,
214
+ prompt_version_id=prompt_version_id,
215
+ prompt_id=prompt_id,
216
+ use_prompt=use_prompt,
217
+ models=models,
218
+ scorer_ids=scorer_ids,
219
+ evaluator_ids=evaluator_ids,
220
+ deep_eval=deep_eval,
221
+ stability_runs=stability_runs,
222
+ row_limit=row_limit,
223
+ concurrency=concurrency,
224
+ metadata=metadata,
225
+ )
226
+ return _unwrap_experiment(self._http.run_experiment(body))
227
+
228
+ def run_by_id(self, experiment_id: str) -> Dict[str, Any]:
229
+ """Execute a pending experiment created via ``create``."""
230
+ if not experiment_id:
231
+ raise ValueError("trodo: run_by_id requires an experiment id")
232
+ return _unwrap_experiment(self._http.run_experiment_by_id(experiment_id))
233
+
234
+ def get(self, experiment_id: str) -> Dict[str, Any]:
235
+ """Fetch an experiment by id."""
236
+ if not experiment_id:
237
+ raise ValueError("trodo: get requires an experiment id")
238
+ res = self._http.get_experiment(experiment_id)
239
+ if isinstance(res, dict) and res.get("__error"):
240
+ raise RuntimeError(f"trodo: get failed (HTTP {res.get('status')})")
241
+ return _unwrap_experiment(res)
242
+
243
+ def wait(
244
+ self,
245
+ experiment_id: str,
246
+ *,
247
+ interval_ms: int = 2500,
248
+ timeout_ms: int = 600_000,
249
+ ) -> Dict[str, Any]:
250
+ """Poll until the experiment reaches a terminal status."""
251
+ deadline = time.monotonic() + (timeout_ms / 1000.0)
252
+ while True:
253
+ exp = self.get(experiment_id)
254
+ status = str(exp.get("status") or "").lower()
255
+ if status in _TERMINAL:
256
+ return exp
257
+ if time.monotonic() >= deadline:
258
+ raise TimeoutError(
259
+ f"trodo: wait timed out after {timeout_ms}ms (status: {status or 'unknown'})"
260
+ )
261
+ time.sleep(interval_ms / 1000.0)
262
+
263
+ def compare(
264
+ self, a_id: str, b_id: str, *, strict: bool = False
265
+ ) -> Dict[str, Any]:
266
+ """Compare two experiments side by side."""
267
+ if not a_id or not b_id:
268
+ raise ValueError("trodo: compare requires two experiment ids")
269
+ res = self._http.compare_experiments(a_id, b_id, strict=strict)
270
+ if isinstance(res, dict) and res.get("__error"):
271
+ raise RuntimeError(f"trodo: compare failed (HTTP {res.get('status')})")
272
+ return res if isinstance(res, dict) else {}
273
+
274
+ @staticmethod
275
+ def _validate_run_params(
276
+ dataset: Optional[str],
277
+ dataset_id: Optional[str],
278
+ models: Optional[List[Dict[str, Any]]],
279
+ ) -> None:
280
+ if not models:
281
+ raise ValueError("trodo: run requires a non-empty models list")
282
+ if not dataset and not dataset_id:
283
+ raise ValueError("trodo: run requires dataset or dataset_id")
284
+ for model in models:
285
+ if not model.get("credential_id") or not model.get("model"):
286
+ raise ValueError("trodo: each model needs credential_id and model")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.16.0
3
+ Version: 2.17.0
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -1,16 +1,16 @@
1
- trodo/__init__.py,sha256=V2vh8SxMkgOaBdowPlHzx_2hpX4T1lUlS2JmE6usz3M,23362
2
- trodo/client.py,sha256=syz4zzWokdBaFNnO5QhYnUKVNHf8KaunofVFc1ODohI,20187
1
+ trodo/__init__.py,sha256=AzLMYxJTUCWHMNOIzgAVcQpYovCA_885ut3JcKqdHOM,24866
2
+ trodo/client.py,sha256=F5-YJH0Kf9XNQYVFXI0MTGGNEMVN5EskKazZEHszaJc,20196
3
3
  trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
4
4
  trodo/user_context.py,sha256=9la6azzwEanVmdP4ps_xMoufbeWVeIGU-M8ychmgajg,7859
5
5
  trodo/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  trodo/api/async_client.py,sha256=rZN4aJ2QiKyrHBK260bApCUB9JaMWU6BQtzoSJZh7xk,3408
7
- trodo/api/endpoints.py,sha256=JVTn5l1U-7U2ibQoR9M_AjRgK6GMRgSx_DkV4opWouo,1171
8
- trodo/api/http_client.py,sha256=rWeRLqVXQ-iXgAIzg6701WtK5eSlPZTTJgKZFerLZhs,8085
7
+ trodo/api/endpoints.py,sha256=TgXhV9WxQ16G1ORukYT9_PdGmx0prBFFa6fl-0GJLIY,1350
8
+ trodo/api/http_client.py,sha256=cF6mFLuTVZgho4TqvO05vM1H6JaxTqujxA6vGeDqaQ8,9032
9
9
  trodo/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trodo/auto/auto_event_manager.py,sha256=cztuRsRkNoJE5R4NfSfTrTJTGl4jx2Yb-Ncy0aVAPo8,4247
11
11
  trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  trodo/managers/dataset_manager.py,sha256=gx0S8ujG3cbw4IskBNMdceBjDbcEh_nebwKSzEvm7hE,2827
13
- trodo/managers/experiment_manager.py,sha256=9GO1JUuV7FykaPfZXI8NVvEBDS2CZuyw9I3ywCrZBTI,3916
13
+ trodo/managers/experiment_manager.py,sha256=V-vemLtenfSI5VIcP0HPiXzEy8AyTrRrbJVCA9Z6wjo,10579
14
14
  trodo/managers/group_manager.py,sha256=ki3Se3qEoSZfREX63oeDeBmEfZF-ISHLE8azEtLg0tM,3542
15
15
  trodo/managers/people_manager.py,sha256=mMVnx40Mlifx6NGgChvohC9ViK6dQu2mkXNHbV8pK1E,2882
16
16
  trodo/managers/prompt_manager.py,sha256=jFHkdvDSxvvb9c53EQvBQygdDJ7X-ylNWEY_ag_SeC0,11227
@@ -33,7 +33,7 @@ trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,87
33
33
  trodo/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  trodo/session/server_session.py,sha256=McsudEiq33XDq3nqxgzBcUvIjQxCMscwEuAPnYXrTjs,2136
35
35
  trodo/session/session_manager.py,sha256=JrgH1VeicmtlxPR4dXEuJbxhi23OelkgwW3-9Slv80o,2525
36
- trodo_python-2.16.0.dist-info/METADATA,sha256=i4J0WgohVDAcQi8OCVa_x6WpDhH9mWeYCEgX8BQ2lLs,23115
37
- trodo_python-2.16.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
38
- trodo_python-2.16.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
39
- trodo_python-2.16.0.dist-info/RECORD,,
36
+ trodo_python-2.17.0.dist-info/METADATA,sha256=5MXB-Qq17K35ZJ41p6g0_1JcOrCgLa3-vycGNB8O_vg,23115
37
+ trodo_python-2.17.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
38
+ trodo_python-2.17.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
39
+ trodo_python-2.17.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (83.0.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5