databar 2.1.0__tar.gz → 2.2.0__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 (29) hide show
  1. {databar-2.1.0/src/databar.egg-info → databar-2.2.0}/PKG-INFO +39 -3
  2. {databar-2.1.0 → databar-2.2.0}/README.md +38 -2
  3. {databar-2.1.0 → databar-2.2.0}/pyproject.toml +1 -1
  4. {databar-2.1.0 → databar-2.2.0}/src/databar/__init__.py +9 -1
  5. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/_guide.py +18 -0
  6. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/app.py +2 -1
  7. databar-2.2.0/src/databar/cli/flows.py +122 -0
  8. {databar-2.1.0 → databar-2.2.0}/src/databar/client.py +59 -3
  9. {databar-2.1.0 → databar-2.2.0}/src/databar/models.py +62 -1
  10. {databar-2.1.0 → databar-2.2.0/src/databar.egg-info}/PKG-INFO +39 -3
  11. {databar-2.1.0 → databar-2.2.0}/src/databar.egg-info/SOURCES.txt +1 -0
  12. {databar-2.1.0 → databar-2.2.0}/tests/test_cli.py +45 -1
  13. {databar-2.1.0 → databar-2.2.0}/tests/test_client.py +38 -0
  14. {databar-2.1.0 → databar-2.2.0}/LICENSE +0 -0
  15. {databar-2.1.0 → databar-2.2.0}/setup.cfg +0 -0
  16. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/__init__.py +0 -0
  17. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/_auth.py +0 -0
  18. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/_onboard.py +0 -0
  19. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/_output.py +0 -0
  20. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/enrichments.py +0 -0
  21. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/tables.py +0 -0
  22. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/tasks.py +0 -0
  23. {databar-2.1.0 → databar-2.2.0}/src/databar/cli/waterfalls.py +0 -0
  24. {databar-2.1.0 → databar-2.2.0}/src/databar/exceptions.py +0 -0
  25. {databar-2.1.0 → databar-2.2.0}/src/databar.egg-info/dependency_links.txt +0 -0
  26. {databar-2.1.0 → databar-2.2.0}/src/databar.egg-info/entry_points.txt +0 -0
  27. {databar-2.1.0 → databar-2.2.0}/src/databar.egg-info/requires.txt +0 -0
  28. {databar-2.1.0 → databar-2.2.0}/src/databar.egg-info/top_level.txt +0 -0
  29. {databar-2.1.0 → databar-2.2.0}/tests/test_new_features.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databar
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Official Databar.ai Python SDK and CLI — connect to enrichments, waterfalls, and tables via api.databar.ai
5
5
  Author-email: "Databar.ai Team" <info@databar.ai>
6
6
  License: MIT License
@@ -55,7 +55,7 @@ Dynamic: license-file
55
55
 
56
56
  # Databar Python SDK
57
57
 
58
- Official Python SDK and CLI for [Databar.ai](https://databar.ai) — run data enrichments, waterfall lookups, and manage tables via `api.databar.ai/v1`.
58
+ Official Python SDK and CLI for [Databar.ai](https://databar.ai) — run data enrichments, waterfall lookups, flows, and manage tables via `api.databar.ai/v1`.
59
59
 
60
60
  [![PyPI](https://img.shields.io/pypi/v/databar-ai)](https://pypi.org/project/databar-ai/)
61
61
  [![Python](https://img.shields.io/pypi/pyversions/databar-ai)](https://pypi.org/project/databar-ai/)
@@ -154,7 +154,8 @@ data = client.run_enrichment_sync(123, {"email": "alice@example.com"})
154
154
  # Run with pagination (for list-style enrichments)
155
155
  data = client.run_enrichment_sync(123, {"query": "CEO"}, pages=3)
156
156
 
157
- # Bulk run
157
+ # Bulk run — results are aligned to inputs: one element per input, in input
158
+ # order, with None for inputs that returned no data (len(data) == len(inputs)).
158
159
  data = client.run_enrichment_bulk_sync(123, [
159
160
  {"email": "alice@example.com"},
160
161
  {"email": "bob@example.com"},
@@ -192,6 +193,26 @@ results = client.run_waterfall_bulk_sync(
192
193
  )
193
194
  ```
194
195
 
196
+ ### Flows
197
+
198
+ ```python
199
+ # List saved flows
200
+ flows = client.list_flows()
201
+
202
+ # Inspect a flow's declared inputs
203
+ flow = client.get_flow("flow-uuid")
204
+ for inp in flow.inputs:
205
+ print(f" {inp.id} (required={inp.required}): {inp.description}")
206
+
207
+ # Run a flow (submit + poll in one call).
208
+ # inputs maps each flow input id → value.
209
+ result = client.run_flow_sync("flow-uuid", {"email": "alice@example.com"})
210
+
211
+ # Or submit and poll manually
212
+ task = client.run_flow("flow-uuid", {"email": "alice@example.com"})
213
+ result = client.poll_task(task.task_id)
214
+ ```
215
+
195
216
  ### Tables
196
217
 
197
218
  ```python
@@ -427,6 +448,21 @@ databar waterfall run email_getter --params '{"linkedin_url": "https://linkedin.
427
448
  databar waterfall bulk email_getter --input leads.csv --out results.csv
428
449
  ```
429
450
 
451
+ ### Flows
452
+
453
+ ```bash
454
+ # List saved flows
455
+ databar flow list
456
+ databar flow list --query "buyer"
457
+
458
+ # Get flow details (declared inputs)
459
+ databar flow get <flow-id>
460
+
461
+ # Run a flow (--inputs maps each flow input id → value)
462
+ databar flow run <flow-id> --inputs '{"email": "alice@example.com"}'
463
+ databar flow run <flow-id> --inputs '{"email": "alice@example.com"}' --format json
464
+ ```
465
+
430
466
  ### Tables
431
467
 
432
468
  ```bash
@@ -1,6 +1,6 @@
1
1
  # Databar Python SDK
2
2
 
3
- Official Python SDK and CLI for [Databar.ai](https://databar.ai) — run data enrichments, waterfall lookups, and manage tables via `api.databar.ai/v1`.
3
+ Official Python SDK and CLI for [Databar.ai](https://databar.ai) — run data enrichments, waterfall lookups, flows, and manage tables via `api.databar.ai/v1`.
4
4
 
5
5
  [![PyPI](https://img.shields.io/pypi/v/databar-ai)](https://pypi.org/project/databar-ai/)
6
6
  [![Python](https://img.shields.io/pypi/pyversions/databar-ai)](https://pypi.org/project/databar-ai/)
@@ -99,7 +99,8 @@ data = client.run_enrichment_sync(123, {"email": "alice@example.com"})
99
99
  # Run with pagination (for list-style enrichments)
100
100
  data = client.run_enrichment_sync(123, {"query": "CEO"}, pages=3)
101
101
 
102
- # Bulk run
102
+ # Bulk run — results are aligned to inputs: one element per input, in input
103
+ # order, with None for inputs that returned no data (len(data) == len(inputs)).
103
104
  data = client.run_enrichment_bulk_sync(123, [
104
105
  {"email": "alice@example.com"},
105
106
  {"email": "bob@example.com"},
@@ -137,6 +138,26 @@ results = client.run_waterfall_bulk_sync(
137
138
  )
138
139
  ```
139
140
 
141
+ ### Flows
142
+
143
+ ```python
144
+ # List saved flows
145
+ flows = client.list_flows()
146
+
147
+ # Inspect a flow's declared inputs
148
+ flow = client.get_flow("flow-uuid")
149
+ for inp in flow.inputs:
150
+ print(f" {inp.id} (required={inp.required}): {inp.description}")
151
+
152
+ # Run a flow (submit + poll in one call).
153
+ # inputs maps each flow input id → value.
154
+ result = client.run_flow_sync("flow-uuid", {"email": "alice@example.com"})
155
+
156
+ # Or submit and poll manually
157
+ task = client.run_flow("flow-uuid", {"email": "alice@example.com"})
158
+ result = client.poll_task(task.task_id)
159
+ ```
160
+
140
161
  ### Tables
141
162
 
142
163
  ```python
@@ -372,6 +393,21 @@ databar waterfall run email_getter --params '{"linkedin_url": "https://linkedin.
372
393
  databar waterfall bulk email_getter --input leads.csv --out results.csv
373
394
  ```
374
395
 
396
+ ### Flows
397
+
398
+ ```bash
399
+ # List saved flows
400
+ databar flow list
401
+ databar flow list --query "buyer"
402
+
403
+ # Get flow details (declared inputs)
404
+ databar flow get <flow-id>
405
+
406
+ # Run a flow (--inputs maps each flow input id → value)
407
+ databar flow run <flow-id> --inputs '{"email": "alice@example.com"}'
408
+ databar flow run <flow-id> --inputs '{"email": "alice@example.com"}' --format json
409
+ ```
410
+
375
411
  ### Tables
376
412
 
377
413
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "databar"
7
- version = "2.1.0"
7
+ version = "2.2.0"
8
8
  description = "Official Databar.ai Python SDK and CLI — connect to enrichments, waterfalls, and tables via api.databar.ai"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -59,6 +59,10 @@ from .models import (
59
59
  # Waterfalls
60
60
  Waterfall,
61
61
  WaterfallEnrichment,
62
+ # Flows
63
+ Flow,
64
+ FlowInput,
65
+ FlowOutput,
62
66
  # Tables
63
67
  Table,
64
68
  Column,
@@ -95,7 +99,7 @@ from .models import (
95
99
  Folder,
96
100
  )
97
101
 
98
- __version__ = "2.1.0"
102
+ __version__ = "2.2.0"
99
103
  __all__ = [
100
104
  "DatabarClient",
101
105
  # exceptions
@@ -131,6 +135,10 @@ __all__ = [
131
135
  # waterfalls
132
136
  "Waterfall",
133
137
  "WaterfallEnrichment",
138
+ # flows
139
+ "Flow",
140
+ "FlowInput",
141
+ "FlowOutput",
134
142
  # tables
135
143
  "Table",
136
144
  "Column",
@@ -128,6 +128,16 @@ databar waterfall run <identifier> --params '{"key": "value"}' --format json
128
128
  databar waterfall bulk <identifier> --input data.csv --out results.csv
129
129
  ```
130
130
 
131
+ ### Flows
132
+ ```bash
133
+ databar flow list --format json
134
+ databar flow get <flow-id> --format json # declared inputs
135
+ databar flow run <flow-id> --inputs '{"input_id": "value"}' --format json
136
+ ```
137
+
138
+ NOTE: `flow run` uses `--inputs` (maps each flow input id → value), NOT `--params`.
139
+ See `flow get` for the list of declared inputs.
140
+
131
141
  ### Tables
132
142
  ```bash
133
143
  databar table list --format json
@@ -174,6 +184,13 @@ result = client.run_enrichment_sync(123, {"email": "alice@example.com"})
174
184
  result = client.run_waterfall_sync("email_getter", {"linkedin_url": "..."})
175
185
  # waterfall.identifier (also .slug) → slug like "email_getter"
176
186
 
187
+ # Flows
188
+ flows = client.list_flows()
189
+ flow = client.get_flow("flow-uuid")
190
+ # flow.id (also .identifier) → flow UUID
191
+ # flow.inputs[i].id → input key to use in the run inputs dict
192
+ result = client.run_flow_sync("flow-uuid", {"email": "alice@example.com"})
193
+
177
194
  # Tables
178
195
  tables = client.list_tables()
179
196
  # table.identifier (also .id, .uuid) → UUID string
@@ -201,6 +218,7 @@ client.create_rows(table.identifier, [InsertRow(fields={"email": "alice@example.
201
218
 
202
219
  - `Table`: `.id`, `.uuid` → `.identifier`
203
220
  - `Waterfall`: `.slug` → `.identifier`
221
+ - `Flow`: `.identifier` → `.id`
204
222
  - `EnrichmentParam`: `.slug` → `.name`, `.label` → `.description`, `.required` → `.is_required`
205
223
  - `EnrichmentResponseField`: `.slug` → `.name`
206
224
 
@@ -10,7 +10,7 @@ import typer
10
10
 
11
11
  from databar import __version__
12
12
 
13
- from . import enrichments, tables, tasks, waterfalls
13
+ from . import enrichments, flows, tables, tasks, waterfalls
14
14
  from ._auth import app as auth_app
15
15
 
16
16
  app = typer.Typer(
@@ -29,6 +29,7 @@ app.add_typer(auth_app, name=None) # merged at root level
29
29
  # Subcommand groups
30
30
  app.add_typer(enrichments.app, name="enrich")
31
31
  app.add_typer(waterfalls.app, name="waterfall")
32
+ app.add_typer(flows.app, name="flow")
32
33
  app.add_typer(tables.app, name="table")
33
34
  app.add_typer(tasks.app, name="task")
34
35
 
@@ -0,0 +1,122 @@
1
+ """
2
+ CLI commands for flows.
3
+
4
+ databar flow list [--query] [--format]
5
+ databar flow get <flow-id> [--format]
6
+ databar flow run <flow-id> --inputs '{"email":"a@b.com"}' [--format] [--raw]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from typing import Optional
13
+
14
+ import typer
15
+
16
+ from databar.exceptions import DatabarError
17
+
18
+ from ._auth import get_client
19
+ from ._output import OutputFormat, console, error, info, output
20
+
21
+ app = typer.Typer(help="List and run saved flows.")
22
+
23
+
24
+ @app.command("list")
25
+ def list_flows(
26
+ query: Optional[str] = typer.Option(None, "--query", "-q", help="Filter by name/description."),
27
+ fmt: OutputFormat = typer.Option(OutputFormat.TABLE, "--format", "--output", "-f"),
28
+ ) -> None:
29
+ """List saved flows in the workspace."""
30
+ client = get_client()
31
+ try:
32
+ flows = client.list_flows()
33
+ except DatabarError as e:
34
+ error(str(e))
35
+ finally:
36
+ client.close()
37
+
38
+ if query:
39
+ q = query.lower()
40
+ flows = [
41
+ f for f in flows
42
+ if q in f.name.lower() or q in f.description.lower() or q in f.id.lower()
43
+ ]
44
+
45
+ if not flows:
46
+ info("No flows found.")
47
+ return
48
+
49
+ rows = [
50
+ {
51
+ "id": f.id,
52
+ "name": f.name,
53
+ "inputs": len(f.inputs),
54
+ "description": f.description[:60] + ("…" if len(f.description) > 60 else ""),
55
+ }
56
+ for f in flows
57
+ ]
58
+ output(rows, fmt, table_columns=["id", "name", "inputs", "description"])
59
+
60
+
61
+ @app.command("get")
62
+ def get_flow(
63
+ flow_id: str = typer.Argument(..., help="Flow id."),
64
+ fmt: OutputFormat = typer.Option(OutputFormat.TABLE, "--format", "--output", "-f"),
65
+ ) -> None:
66
+ """Get details for a specific flow."""
67
+ client = get_client()
68
+ try:
69
+ f = client.get_flow(flow_id)
70
+ except DatabarError as exc:
71
+ error(str(exc))
72
+ finally:
73
+ client.close()
74
+
75
+ if fmt == OutputFormat.JSON:
76
+ output(f.model_dump(), fmt)
77
+ return
78
+
79
+ console.print(f"\n[bold cyan]Flow:[/bold cyan] {f.name} ({f.id})")
80
+ console.print(f"\n{f.description}\n")
81
+
82
+ if f.inputs:
83
+ console.print("[bold]Inputs:[/bold]")
84
+ rows = [
85
+ {
86
+ "id": inp.id,
87
+ "required": "yes" if inp.required else "no",
88
+ "type": inp.type,
89
+ "description": inp.description,
90
+ }
91
+ for inp in f.inputs
92
+ ]
93
+ output(rows, OutputFormat.TABLE, table_columns=["id", "required", "type", "description"])
94
+
95
+
96
+ @app.command("run")
97
+ def run_flow(
98
+ flow_id: str = typer.Argument(..., help="Flow id."),
99
+ inputs_json: str = typer.Option(..., "--inputs", "-i", help='JSON inputs, e.g. \'{"email":"a@b.com"}\''),
100
+ fmt: OutputFormat = typer.Option(OutputFormat.TABLE, "--format", "--output", "-f"),
101
+ raw: bool = typer.Option(False, "--raw", help="Print raw result without formatting."),
102
+ ) -> None:
103
+ """Run a flow and wait for results."""
104
+ try:
105
+ inputs = json.loads(inputs_json)
106
+ except json.JSONDecodeError as e:
107
+ error(f"Invalid JSON for --inputs: {e}")
108
+
109
+ client = get_client()
110
+ try:
111
+ info(f"Running flow '{flow_id}'…")
112
+ result = client.run_flow_sync(flow_id, inputs)
113
+ except DatabarError as exc:
114
+ error(str(exc))
115
+ finally:
116
+ client.close()
117
+
118
+ if raw:
119
+ console.print(result)
120
+ return
121
+
122
+ output(result, fmt)
@@ -15,6 +15,7 @@ Endpoint groups:
15
15
  - Enrichments: list_enrichments, get_enrichment, run_enrichment[_bulk][_sync],
16
16
  get_param_choices
17
17
  - Waterfalls: list_waterfalls, get_waterfall, run_waterfall[_bulk][_sync]
18
+ - Flows: list_flows, get_flow, run_flow[_sync]
18
19
  - Tables: create_table, list_tables, delete_table, rename_table,
19
20
  get_columns, create_column, rename_column, delete_column,
20
21
  get_table_enrichments, add_enrichment, run_table_enrichment,
@@ -70,6 +71,7 @@ from .models import (
70
71
  ExporterListResponse,
71
72
  ExporterParam,
72
73
  ExporterResponseField,
74
+ Flow,
73
75
  Folder,
74
76
  InsertOptions,
75
77
  InsertRow,
@@ -265,6 +267,11 @@ class DatabarClient:
265
267
  Returns the task's data payload on success (status completed or partially_completed).
266
268
  Raises DatabarTaskFailedError, DatabarGoneError or DatabarTimeoutError otherwise.
267
269
 
270
+ For bulk runs the payload is a list aligned to the inputs: one element
271
+ per input in input order, with ``None`` for inputs that returned no data
272
+ (so ``len(data) == number of inputs`` and ``data[i]`` ↔ ``input[i]``). A
273
+ single run returns the bare result object.
274
+
268
275
  Task data is stored for 24 hours. After that the status becomes 'gone'.
269
276
  """
270
277
  for _ in range(self._max_poll_attempts):
@@ -383,6 +390,11 @@ class DatabarClient:
383
390
  """
384
391
  Submit a bulk enrichment run for multiple inputs.
385
392
 
393
+ The polled result is aligned to the inputs: one element per input, in the
394
+ same order as ``params``, with ``None`` for inputs that returned no data.
395
+ So ``len(result) == len(params)`` and ``result[i]`` is the result for
396
+ ``params[i]`` — join results back to inputs by position.
397
+
386
398
  Args:
387
399
  enrichment_id: The enrichment to run.
388
400
  params: List of per-row input parameter dicts.
@@ -410,7 +422,11 @@ class DatabarClient:
410
422
  params: List[Dict[str, Any]],
411
423
  pages: Optional[int] = None,
412
424
  ) -> Any:
413
- """Submit and poll a bulk enrichment, returning final data when complete."""
425
+ """Submit and poll a bulk enrichment, returning final data when complete.
426
+
427
+ Returns a list aligned to ``params``: one element per input in input
428
+ order, ``None`` for misses (``len(result) == len(params)``).
429
+ """
414
430
  task = self.run_enrichment_bulk(enrichment_id, params, pages=pages)
415
431
  return self.poll_task(task.task_id)
416
432
 
@@ -478,7 +494,12 @@ class DatabarClient:
478
494
  enrichments: Optional[List[int]] = None,
479
495
  email_verifier: Optional[int] = None,
480
496
  ) -> RunResponse:
481
- """Submit a bulk waterfall run for multiple inputs."""
497
+ """Submit a bulk waterfall run for multiple inputs.
498
+
499
+ The polled result is aligned to the inputs: one element per input, in the
500
+ same order as ``params``, with ``None`` for inputs that returned no data
501
+ (``len(result) == len(params)``, ``result[i]`` ↔ ``params[i]``).
502
+ """
482
503
  if not enrichments:
483
504
  waterfall = self.get_waterfall(identifier)
484
505
  enrichments = [e.id for e in waterfall.available_enrichments]
@@ -508,10 +529,45 @@ class DatabarClient:
508
529
  enrichments: Optional[List[int]] = None,
509
530
  email_verifier: Optional[int] = None,
510
531
  ) -> Any:
511
- """Submit and poll a bulk waterfall, returning final data when complete."""
532
+ """Submit and poll a bulk waterfall, returning final data when complete.
533
+
534
+ Returns a list aligned to ``params``: one element per input in input
535
+ order, ``None`` for misses (``len(result) == len(params)``).
536
+ """
512
537
  task = self.run_waterfall_bulk(identifier, params, enrichments, email_verifier)
513
538
  return self.poll_task(task.task_id)
514
539
 
540
+ # -----------------------------------------------------------------------
541
+ # Flows
542
+ # -----------------------------------------------------------------------
543
+
544
+ def list_flows(self) -> List[Flow]:
545
+ """List all saved flows in the workspace."""
546
+ data = self._request("GET", "/flows")
547
+ return [Flow.model_validate(f) for f in data]
548
+
549
+ def get_flow(self, flow_id: str) -> Flow:
550
+ """Get details for a specific flow, including its declared inputs."""
551
+ data = self._request("GET", f"/flows/{flow_id}")
552
+ return Flow.model_validate(data)
553
+
554
+ def run_flow(self, flow_id: str, inputs: Dict[str, str]) -> RunResponse:
555
+ """
556
+ Submit a flow run. Returns a task — use poll_task() or run_flow_sync().
557
+
558
+ Args:
559
+ flow_id: The flow UUID (from list_flows / get_flow).
560
+ inputs: Maps each flow input id → value. See get_flow() for the
561
+ list of declared inputs.
562
+ """
563
+ data = self._request("POST", f"/flows/{flow_id}/run", json={"inputs": inputs})
564
+ return RunResponse.model_validate(data)
565
+
566
+ def run_flow_sync(self, flow_id: str, inputs: Dict[str, str]) -> Any:
567
+ """Submit and poll a flow, returning final data when complete."""
568
+ task = self.run_flow(flow_id, inputs)
569
+ return self.poll_task(task.task_id)
570
+
515
571
  # -----------------------------------------------------------------------
516
572
  # Tables — CRUD
517
573
  # -----------------------------------------------------------------------
@@ -261,7 +261,12 @@ class TaskResponse(BaseModel):
261
261
  )
262
262
  data: Optional[Union[List[Any], Dict[str, Any]]] = Field(
263
263
  default=None,
264
- description="Resulting data once completed.",
264
+ description=(
265
+ "Resulting data once completed. For bulk runs this is a list aligned to "
266
+ "the inputs: one element per input in input order, with null for inputs "
267
+ "that returned no data (len(data) == number of inputs). A single run "
268
+ "returns the bare result object."
269
+ ),
265
270
  )
266
271
  error: Optional[Union[str, List[str]]] = None
267
272
  credits_spent: float = 0
@@ -315,6 +320,62 @@ class Waterfall(BaseModel):
315
320
  return self.identifier
316
321
 
317
322
 
323
+ # ===========================================================================
324
+ # Flows
325
+ # ===========================================================================
326
+
327
+
328
+ class FlowInput(BaseModel):
329
+ """A declared input of a flow.
330
+
331
+ Fields: id, description, type, required.
332
+
333
+ The ``id`` is the key to use in the inputs dict passed to run_flow().
334
+ """
335
+
336
+ id: str = Field(description="Input id — the key to use in the run_flow() inputs dict.")
337
+ description: str
338
+ type: str
339
+ required: bool
340
+
341
+
342
+ class FlowOutput(BaseModel):
343
+ """A field produced by a flow node.
344
+
345
+ Fields: id, response_field_id.
346
+ """
347
+
348
+ id: str
349
+ response_field_id: Optional[int] = None
350
+
351
+
352
+ class Flow(BaseModel):
353
+ """A saved workspace flow (multi-step enrichment pipeline).
354
+
355
+ Fields: id, name, description, inputs, outputs, created_at, updated_at.
356
+
357
+ Property aliases: .identifier → .id.
358
+
359
+ Usage::
360
+
361
+ flow = client.get_flow("flow-uuid")
362
+ result = client.run_flow_sync(flow.id, {"email": "alice@example.com"})
363
+ """
364
+
365
+ id: str = Field(description="Flow UUID. Use this when calling get_flow()/run_flow().")
366
+ name: str
367
+ description: str
368
+ inputs: List[FlowInput] = Field(default_factory=list)
369
+ outputs: List[FlowOutput] = Field(default_factory=list)
370
+ created_at: str = ""
371
+ updated_at: str = ""
372
+
373
+ @property
374
+ def identifier(self) -> str:
375
+ """Alias for id."""
376
+ return self.id
377
+
378
+
318
379
  # ===========================================================================
319
380
  # Tables
320
381
  # ===========================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databar
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Official Databar.ai Python SDK and CLI — connect to enrichments, waterfalls, and tables via api.databar.ai
5
5
  Author-email: "Databar.ai Team" <info@databar.ai>
6
6
  License: MIT License
@@ -55,7 +55,7 @@ Dynamic: license-file
55
55
 
56
56
  # Databar Python SDK
57
57
 
58
- Official Python SDK and CLI for [Databar.ai](https://databar.ai) — run data enrichments, waterfall lookups, and manage tables via `api.databar.ai/v1`.
58
+ Official Python SDK and CLI for [Databar.ai](https://databar.ai) — run data enrichments, waterfall lookups, flows, and manage tables via `api.databar.ai/v1`.
59
59
 
60
60
  [![PyPI](https://img.shields.io/pypi/v/databar-ai)](https://pypi.org/project/databar-ai/)
61
61
  [![Python](https://img.shields.io/pypi/pyversions/databar-ai)](https://pypi.org/project/databar-ai/)
@@ -154,7 +154,8 @@ data = client.run_enrichment_sync(123, {"email": "alice@example.com"})
154
154
  # Run with pagination (for list-style enrichments)
155
155
  data = client.run_enrichment_sync(123, {"query": "CEO"}, pages=3)
156
156
 
157
- # Bulk run
157
+ # Bulk run — results are aligned to inputs: one element per input, in input
158
+ # order, with None for inputs that returned no data (len(data) == len(inputs)).
158
159
  data = client.run_enrichment_bulk_sync(123, [
159
160
  {"email": "alice@example.com"},
160
161
  {"email": "bob@example.com"},
@@ -192,6 +193,26 @@ results = client.run_waterfall_bulk_sync(
192
193
  )
193
194
  ```
194
195
 
196
+ ### Flows
197
+
198
+ ```python
199
+ # List saved flows
200
+ flows = client.list_flows()
201
+
202
+ # Inspect a flow's declared inputs
203
+ flow = client.get_flow("flow-uuid")
204
+ for inp in flow.inputs:
205
+ print(f" {inp.id} (required={inp.required}): {inp.description}")
206
+
207
+ # Run a flow (submit + poll in one call).
208
+ # inputs maps each flow input id → value.
209
+ result = client.run_flow_sync("flow-uuid", {"email": "alice@example.com"})
210
+
211
+ # Or submit and poll manually
212
+ task = client.run_flow("flow-uuid", {"email": "alice@example.com"})
213
+ result = client.poll_task(task.task_id)
214
+ ```
215
+
195
216
  ### Tables
196
217
 
197
218
  ```python
@@ -427,6 +448,21 @@ databar waterfall run email_getter --params '{"linkedin_url": "https://linkedin.
427
448
  databar waterfall bulk email_getter --input leads.csv --out results.csv
428
449
  ```
429
450
 
451
+ ### Flows
452
+
453
+ ```bash
454
+ # List saved flows
455
+ databar flow list
456
+ databar flow list --query "buyer"
457
+
458
+ # Get flow details (declared inputs)
459
+ databar flow get <flow-id>
460
+
461
+ # Run a flow (--inputs maps each flow input id → value)
462
+ databar flow run <flow-id> --inputs '{"email": "alice@example.com"}'
463
+ databar flow run <flow-id> --inputs '{"email": "alice@example.com"}' --format json
464
+ ```
465
+
430
466
  ### Tables
431
467
 
432
468
  ```bash
@@ -18,6 +18,7 @@ src/databar/cli/_onboard.py
18
18
  src/databar/cli/_output.py
19
19
  src/databar/cli/app.py
20
20
  src/databar/cli/enrichments.py
21
+ src/databar/cli/flows.py
21
22
  src/databar/cli/tables.py
22
23
  src/databar/cli/tasks.py
23
24
  src/databar/cli/waterfalls.py
@@ -28,6 +28,8 @@ from databar.models import (
28
28
  Enrichment,
29
29
  EnrichmentParam,
30
30
  EnrichmentSummary,
31
+ Flow,
32
+ FlowInput,
31
33
  Table,
32
34
  TableEnrichment,
33
35
  TaskResponse,
@@ -88,6 +90,18 @@ def _client_mock(**method_overrides):
88
90
  m.run_waterfall_sync.return_value = [{"email": "alice@example.com"}]
89
91
  m.run_waterfall_bulk_sync.return_value = [{"email": "alice@example.com"}]
90
92
 
93
+ m.list_flows.return_value = [
94
+ Flow(
95
+ id="flow-uuid-1",
96
+ name="Find buyer",
97
+ description="Enrich a lead",
98
+ inputs=[FlowInput(id="email", description="Email", type="text", required=True)],
99
+ outputs=[],
100
+ )
101
+ ]
102
+ m.get_flow.return_value = m.list_flows.return_value[0]
103
+ m.run_flow_sync.return_value = {"full_name": "Alice"}
104
+
91
105
  m.list_tables.return_value = [
92
106
  Table(identifier="tbl-1", name="My Table", created_at="2024-01-01", updated_at="2024-01-01")
93
107
  ]
@@ -241,6 +255,36 @@ def test_waterfall_run(monkeypatch):
241
255
  mock.run_waterfall_sync.assert_called_once()
242
256
 
243
257
 
258
+ # ===========================================================================
259
+ # Flows
260
+ # ===========================================================================
261
+
262
+
263
+ def test_flow_list(monkeypatch):
264
+ mock = _client_mock()
265
+ monkeypatch.setattr("databar.cli.flows.get_client", lambda: mock)
266
+ result = invoke(["flow", "list"])
267
+ assert result.exit_code == 0
268
+ assert "flow-uuid-1" in result.output
269
+ assert "Find buyer" in result.output
270
+
271
+
272
+ def test_flow_get(monkeypatch):
273
+ mock = _client_mock()
274
+ monkeypatch.setattr("databar.cli.flows.get_client", lambda: mock)
275
+ result = invoke(["flow", "get", "flow-uuid-1"])
276
+ assert result.exit_code == 0
277
+ assert "Find buyer" in result.output
278
+
279
+
280
+ def test_flow_run(monkeypatch):
281
+ mock = _client_mock()
282
+ monkeypatch.setattr("databar.cli.flows.get_client", lambda: mock)
283
+ result = invoke(["flow", "run", "flow-uuid-1", "--inputs", '{"email":"alice@example.com"}'])
284
+ assert result.exit_code == 0
285
+ mock.run_flow_sync.assert_called_once_with("flow-uuid-1", {"email": "alice@example.com"})
286
+
287
+
244
288
  # ===========================================================================
245
289
  # Tables
246
290
  # ===========================================================================
@@ -329,4 +373,4 @@ def test_task_get_poll(monkeypatch):
329
373
  def test_version_flag():
330
374
  result = runner.invoke(app, ["--version"])
331
375
  assert result.exit_code == 0
332
- assert "2.0.9" in result.output
376
+ assert "2.2.0" in result.output
@@ -38,6 +38,7 @@ from .conftest import (
38
38
  BASE_URL,
39
39
  enrichment_payload,
40
40
  enrichment_summary_payload,
41
+ flow_payload,
41
42
  table_payload,
42
43
  task_payload,
43
44
  user_payload,
@@ -200,6 +201,43 @@ def test_run_waterfall_auto_resolves_providers(client: DatabarClient, httpx_mock
200
201
  assert body["enrichments"] == [10, 11]
201
202
 
202
203
 
204
+ # ===========================================================================
205
+ # Flows
206
+ # ===========================================================================
207
+
208
+
209
+ def test_list_flows(client: DatabarClient, httpx_mock: HTTPXMock):
210
+ httpx_mock.add_response(url=f"{BASE_URL}/flows", json=[flow_payload()])
211
+ result = client.list_flows()
212
+ assert len(result) == 1
213
+ assert result[0].id == "flow-uuid-1"
214
+ assert result[0].identifier == "flow-uuid-1"
215
+ assert result[0].inputs[0].id == "email"
216
+
217
+
218
+ def test_get_flow(client: DatabarClient, httpx_mock: HTTPXMock):
219
+ httpx_mock.add_response(url=f"{BASE_URL}/flows/flow-uuid-1", json=flow_payload())
220
+ f = client.get_flow("flow-uuid-1")
221
+ assert f.name == "Find buyer"
222
+ assert f.inputs[0].required is True
223
+
224
+
225
+ def test_run_flow_returns_task(client: DatabarClient, httpx_mock: HTTPXMock):
226
+ httpx_mock.add_response(url=f"{BASE_URL}/flows/flow-uuid-1/run", json=task_payload("processing"))
227
+ task = client.run_flow("flow-uuid-1", {"email": "alice@example.com"})
228
+ assert task.task_id == "task-123"
229
+ req = httpx_mock.get_requests()[-1]
230
+ body = json.loads(req.content)
231
+ assert body == {"inputs": {"email": "alice@example.com"}}
232
+
233
+
234
+ def test_run_flow_sync(client: DatabarClient, httpx_mock: HTTPXMock):
235
+ httpx_mock.add_response(url=f"{BASE_URL}/flows/flow-uuid-1/run", json=task_payload("processing"))
236
+ httpx_mock.add_response(url=f"{BASE_URL}/tasks/task-123", json=task_payload("completed", data={"full_name": "Alice"}))
237
+ result = client.run_flow_sync("flow-uuid-1", {"email": "alice@example.com"})
238
+ assert result == {"full_name": "Alice"}
239
+
240
+
203
241
  # ===========================================================================
204
242
  # Tables
205
243
  # ===========================================================================
File without changes
File without changes