overscore-cli 0.1.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.
- overscore_cli/__init__.py +4 -0
- overscore_cli/ovs.py +1766 -0
- overscore_cli/skills/overscore/SKILL.md +52 -0
- overscore_cli/skills/overscore-model/SKILL.md +45 -0
- overscore_cli/skills/overscore-model/model.py +29 -0
- overscore_cli/skills/overscore-model/model.toml +15 -0
- overscore_cli/skills/overscore-sync/SKILL.md +37 -0
- overscore_cli-0.1.0.dist-info/METADATA +39 -0
- overscore_cli-0.1.0.dist-info/RECORD +12 -0
- overscore_cli-0.1.0.dist-info/WHEEL +4 -0
- overscore_cli-0.1.0.dist-info/entry_points.txt +2 -0
- overscore_model.py +75 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: overscore
|
|
3
|
+
description: "Work with a company's data in Overscore from an agent: what the objects are (dishes and tables, sources, models, files, destinations and syncs, runs), how to connect with the ovs command and a key, how to find things, and the rules for touching the data. Read this before any other overscore-* skill."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Overscore
|
|
7
|
+
|
|
8
|
+
Overscore holds a company's data and runs the work on it: data comes in from their systems, models turn it into the tables people and agents use, and syncs send results back out. Everything is reachable with the `ovs` command (and the same doors over MCP).
|
|
9
|
+
|
|
10
|
+
## Connect
|
|
11
|
+
|
|
12
|
+
uv tool install overscore-cli # or: pip install overscore-cli
|
|
13
|
+
ovs login # the person approves this computer in the browser; the key goes in the keychain
|
|
14
|
+
ovs init --dish <dish> # in the project folder: overscore.toml, the rules, these skills
|
|
15
|
+
ovs whoami # the dishes the key reaches, and how
|
|
16
|
+
|
|
17
|
+
Logging in needs the person: they type a code in their browser. Ask them to run `ovs login` in their own terminal
|
|
18
|
+
(in Claude Code: `! ovs login`) rather than running it yourself; it waits up to ten minutes for them.
|
|
19
|
+
Where there is no browser or keychain (a server, a CI job), a key from the Agents screen works instead:
|
|
20
|
+
`export OVERSCORE_KEY=ovs_...` and `export OVERSCORE_URL=https://<customer>.overscore.tech`. Ask the person for it;
|
|
21
|
+
never print it.
|
|
22
|
+
|
|
23
|
+
A key's level per dish: `read` reads, `hot` reads and writes, `pour` only adds rows, `masked` sees stand-ins for personal details. Building models and uploading files take `hot`.
|
|
24
|
+
|
|
25
|
+
## The objects
|
|
26
|
+
|
|
27
|
+
- **dish.table**: a dish is a dataset (one per source or area, like `hubspot` or `sales`); every table is named `dish.table`.
|
|
28
|
+
- **source**: data coming in from a system (HubSpot, Stripe, Shopify, a Postgres, a webhook), landing on a schedule. `ovs connectors`, `ovs connect <name> ...`.
|
|
29
|
+
- **model**: a table built from other tables, in SQL or Python. It rebuilds after what it reads changes. Python models: the `overscore-model` skill.
|
|
30
|
+
- **file** (an upload): a file a model needs, like a fitted model. Versioned; one version is live. `ovs uploads`.
|
|
31
|
+
- **destination** and **sync**: a system data goes to, and a table sent to it. Only rows that changed go. The `overscore-sync` skill.
|
|
32
|
+
- **run**: every model build, sync and landing is a run with a log. `ovs runs`, `ovs run-log <id>`. A run is the receipt.
|
|
33
|
+
|
|
34
|
+
## Find things
|
|
35
|
+
|
|
36
|
+
ovs tables every table the key can read
|
|
37
|
+
ovs describe <dish> <table> columns, types, sample values
|
|
38
|
+
ovs notes <dish> what people have said about the data: read before writing SQL
|
|
39
|
+
ovs query "select ... from dish.table" DuckDB SQL; JSON columns read with ->> (payload->>'id')
|
|
40
|
+
ovs search <word> tables, columns and notes that mention it
|
|
41
|
+
|
|
42
|
+
## What good looks like
|
|
43
|
+
|
|
44
|
+
- You read the notes and the columns before writing anything, and you say which tables an answer came from.
|
|
45
|
+
- Every change is followed by reading its run: the row count, the log, ok or failed.
|
|
46
|
+
- What you report is in the business's words ("scores for 4,210 contacts, back in HubSpot"), not column names.
|
|
47
|
+
|
|
48
|
+
## Three mistakes
|
|
49
|
+
|
|
50
|
+
1. **Guessing at a table.** Raw tables from a source keep the source's shape; look for a model or a signed-off table first (`ovs index` shows the tier of each).
|
|
51
|
+
2. **Calling it done before the run says so.** A save is not a build; `ovs model push` and `ovs sync run` print the run, and a failed run leaves the old table as it was.
|
|
52
|
+
3. **Letting the key or the data out.** The key lives in the keychain (or `OVERSCORE_KEY`), never in a file or message. Pulled samples in `.overscore/` are the company's data: not committed, not pasted.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: overscore-model
|
|
3
|
+
description: "Build and deploy a Python model on Overscore (a lead scorer, a forecast, a dedupe, a data scientist's fitted model; anything that turns tables into a table): pull a sample, write models/<name>/model.py and model.toml, run it locally, upload the files it needs, push it, and read the run. Use when asked to score records, deploy a model, or compute a table in Python."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build and deploy a Python model
|
|
7
|
+
|
|
8
|
+
A Python model is one script that reads tables and returns rows; the rows become the table `dish.<name>`. In Overscore it rebuilds by itself after the tables it reads change, and every build is a run with a log. The templates `model.py` and `model.toml` come with this skill.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. Know the data: `ovs describe <dish> <table>`, `ovs notes <dish>`, `ovs query "select * from <dish>.<table> limit 5"`.
|
|
13
|
+
2. Pull a sample to work on: `ovs pull <dish>.<table>` (10,000 rows at most; `--where "..."` for the rows that matter).
|
|
14
|
+
3. Write `models/<name>/model.py` and `models/<name>/model.toml` from the templates. The folder's name is the model's name.
|
|
15
|
+
4. Put each file it needs at `files/<file name>.<ext>` (a pickle, a lookup).
|
|
16
|
+
5. Run it here until the output is right: `ovs model run <name> --local`. It prints the rows, and the columns and types the table will have.
|
|
17
|
+
6. Send the files: `ovs upload put <file name> files/<file name>.<ext>`. Each put is a new version, live at once.
|
|
18
|
+
7. Push: `ovs model push <name>`. It saves model.py with model.toml's settings, runs it in Overscore, and prints the run's result and log.
|
|
19
|
+
8. Check the table: `ovs query "select count(*) from <dish>.<name>"` and a few rows beside the local output.
|
|
20
|
+
|
|
21
|
+
Later: `ovs model log <name>` shows the last run, `ovs model run <name>` rebuilds by hand, `ovs upload live <file name> <version>` goes back to an older file.
|
|
22
|
+
|
|
23
|
+
## The helper
|
|
24
|
+
|
|
25
|
+
from overscore_model import rows, read, file, output, log
|
|
26
|
+
|
|
27
|
+
- `rows("dish.table")`: the table as a list of dicts; needs no libraries, fine to a few hundred thousand rows.
|
|
28
|
+
- `read("dish.table")`: the path of the table as Parquet, for pandas, polars, pyarrow or duckdb (list them in requirements).
|
|
29
|
+
- `file("name")`: the path of a file's live version (here: `files/<name>.<ext>`).
|
|
30
|
+
- `output(rows)`: the result, a list of dicts or a DataFrame. Call it once.
|
|
31
|
+
- `log("scored 120")`: a line in the run log.
|
|
32
|
+
|
|
33
|
+
A table can be named `dish.table` or just `table`; model.toml's `reads` must list every one the script reads.
|
|
34
|
+
|
|
35
|
+
## What good looks like
|
|
36
|
+
|
|
37
|
+
- The local run and the push agree on columns and types, and the push's run says ok with the full row count (the local run only saw the sample).
|
|
38
|
+
- One row per thing, with the column a sync will match on (`contact_id`), and the model's version in a column so every score can be traced to the file that made it.
|
|
39
|
+
- model.toml lists every table, every file and every library the script uses.
|
|
40
|
+
|
|
41
|
+
## Three mistakes
|
|
42
|
+
|
|
43
|
+
1. **Pushing with a different file than you ran with.** The push says so when `files/<name>` is not the live version in Overscore; `ovs upload put` sends yours.
|
|
44
|
+
2. **Anything beyond model.py.** Only model.py goes up, and in Overscore the script has no network. A helper module or a download that works here fails there: keep it in one file, read only tables and files.
|
|
45
|
+
3. **Saying it worked before reading the run.** A failed run leaves the table as it was and says why in the log; `ovs model push` exits 1 and prints it.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Lead score for every contact, from the fitted model in files/lead_model.pkl.
|
|
2
|
+
|
|
3
|
+
A Python model: runs in Overscore after the tables it reads change; `ovs model run lead_scores --local` runs it here.
|
|
4
|
+
Keep everything in this one file. Read only the tables in model.toml and the files from file(); there is no network.
|
|
5
|
+
"""
|
|
6
|
+
import pickle
|
|
7
|
+
|
|
8
|
+
from overscore_model import file, log, output, rows
|
|
9
|
+
|
|
10
|
+
# The fitted model a data scientist handed over: files/lead_model.pkl here, the live upload in Overscore.
|
|
11
|
+
with open(file("lead_model"), "rb") as f:
|
|
12
|
+
model = pickle.load(f)
|
|
13
|
+
|
|
14
|
+
scored = []
|
|
15
|
+
for r in rows("hubspot.contact"): # each table in model.toml `reads`, as a list of dicts
|
|
16
|
+
features = [float(r.get("num_visits") or 0), float(r.get("num_forms") or 0)]
|
|
17
|
+
scored.append({
|
|
18
|
+
"contact_id": r["id"], # what a sync matches on
|
|
19
|
+
"score": round(float(model.predict_proba([features])[0][1]) * 100, 1),
|
|
20
|
+
"model_version": getattr(model, "version", "v1"),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
# With pandas instead (add pandas and pyarrow to requirements):
|
|
24
|
+
# import pandas as pd
|
|
25
|
+
# from overscore_model import read
|
|
26
|
+
# df = pd.read_parquet(read("hubspot.contact")) # and output(df) takes the DataFrame
|
|
27
|
+
|
|
28
|
+
log(f"scored {len(scored)} contacts")
|
|
29
|
+
output(scored) # once: these rows become the table <dish>.lead_scores
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# The tables model.py reads: dish.table, or table for one in this project's dish.
|
|
2
|
+
reads = ["hubspot.contact"]
|
|
3
|
+
|
|
4
|
+
# The files it needs, by name; the local copy is files/<name>.<ext>, sent with ovs upload put.
|
|
5
|
+
uploads = ["lead_model"]
|
|
6
|
+
|
|
7
|
+
# Every library model.py imports, as pip takes them. Empty means the standard library only.
|
|
8
|
+
requirements = ["scikit-learn==1.5.2"]
|
|
9
|
+
|
|
10
|
+
description = "Lead score for every contact, from the fitted model in files/lead_model.pkl"
|
|
11
|
+
|
|
12
|
+
# When it rebuilds: after_sources (after what it reads changes), every 6h, or manual.
|
|
13
|
+
refresh = "after_sources"
|
|
14
|
+
|
|
15
|
+
# dish = "sales" # only if this model lives in a different dish from overscore.toml's
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: overscore-sync
|
|
3
|
+
description: "Send a table from Overscore to another system and keep it up to date: a destination (a webhook URL of theirs, or a HubSpot portal) and a sync (which table, the column that identifies each record, which columns, and when: after the table changes, on a schedule, or by hand). Only rows that changed are sent. Use when asked to push scores to HubSpot, send results to a CRM, or feed another system."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Send a table somewhere
|
|
7
|
+
|
|
8
|
+
A **destination** is a system data goes to. A **sync** sends one table (or a query) to it: each row is a record, matched on a key column, and only rows whose sent columns changed since they last went are sent. Every send is a run; rows the other side refuses are kept with the reason and go again next time.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. See what exists: `ovs destinations`, `ovs syncs`.
|
|
13
|
+
2. Add the destination if it is not there:
|
|
14
|
+
- A URL of theirs: `ovs destination add webhook "Their CRM" https://...`. Each batch is POSTed as JSON, signed; the signing secret is printed once, for their side to check the `X-Overscore-Signature` header (HMAC-SHA256 of the body).
|
|
15
|
+
- HubSpot: `ovs destination add hubspot "HubSpot" --object contacts --source <dish>.<hubspot source>` borrows the HubSpot source's key (it needs the write scopes), then `ovs destination check "HubSpot"`.
|
|
16
|
+
3. Add the sync: `ovs sync add <name> --from <table> --key <id column> --to "<destination>"`. Options: `--columns score band=lead_band` (column, or column=field at the destination; default every column but the key) and `--after-change` (the default), `--every 15m`, or `--manual`. It is written to syncs.toml.
|
|
17
|
+
4. Send now and read the result: `ovs sync run <name>`. Then `ovs sync show <name>` for its runs and any refused records.
|
|
18
|
+
|
|
19
|
+
To change a sync, edit its entry in syncs.toml and run `ovs sync add <name>` again. `ovs sync pause <name>`, `ovs sync resume <name>`, `ovs sync rm <name>`.
|
|
20
|
+
|
|
21
|
+
## HubSpot
|
|
22
|
+
|
|
23
|
+
- With no `--id-property`, the key column must hold HubSpot's record id, and only existing records are updated.
|
|
24
|
+
- With `--id-property email` (or any unique property), the key column holds that value, and a record that does not exist yet is created.
|
|
25
|
+
- Each column writes the HubSpot property named by its field (`score=hs_lead_score`); a property that does not exist is created, typed from the column.
|
|
26
|
+
|
|
27
|
+
## What good looks like
|
|
28
|
+
|
|
29
|
+
- The first run sends every row; the next sends only the rows that changed, often none.
|
|
30
|
+
- Refused is 0. When it is not, `ovs sync show` gives the reason per record.
|
|
31
|
+
- Only the columns the other system needs leave: the score and its band, not the email list.
|
|
32
|
+
|
|
33
|
+
## Three mistakes
|
|
34
|
+
|
|
35
|
+
1. **The wrong key.** The key must be what the other side matches on: HubSpot's record id, or the `--id-property` value. A wrong key refuses every row, or with an id property, creates duplicates.
|
|
36
|
+
2. **Waiting on after-change.** An after-change sync sends when the table next changes; `ovs sync run <name>` sends the current rows now.
|
|
37
|
+
3. **Sending everything.** Leaving out `--columns` sends every column; name the ones the destination needs.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: overscore-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ovs: the Overscore command line, and the helper a Python model imports
|
|
5
|
+
Project-URL: Homepage, https://overscore.tech
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: duckdb>=1.1
|
|
8
|
+
Requires-Dist: keyring>=25
|
|
9
|
+
Requires-Dist: requests>=2.31
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# overscore-cli
|
|
13
|
+
|
|
14
|
+
`ovs`, the command line for [Overscore](https://overscore.tech): query your data, build and deploy Python models,
|
|
15
|
+
send results to the systems your team works in, from a terminal or from a coding agent like Claude Code.
|
|
16
|
+
|
|
17
|
+
uv tool install overscore-cli # or: pip install overscore-cli
|
|
18
|
+
export OVERSCORE_KEY=ovs_... # from Settings > Keys in Overscore
|
|
19
|
+
ovs init --dish sales # this folder becomes a project: overscore.toml, the rules and skills for your agent
|
|
20
|
+
ovs guide # every command
|
|
21
|
+
|
|
22
|
+
`ovs init --for claude|codex|cursor|gemini|agents|all` writes the rules and skills in your agent's layout
|
|
23
|
+
(CLAUDE.md and .claude/skills, AGENTS.md, .cursor/rules, GEMINI.md).
|
|
24
|
+
|
|
25
|
+
## A Python model, start to finish
|
|
26
|
+
|
|
27
|
+
ovs pull sales.leads # a sample, for local runs
|
|
28
|
+
# write models/lead_scores/model.py and model.toml
|
|
29
|
+
ovs model run lead_scores --local # runs here, on the sample
|
|
30
|
+
ovs upload put lead_model files/lead_model.pkl # the fitted model it reads
|
|
31
|
+
ovs model push lead_scores # saved and run in Overscore; prints the run's log
|
|
32
|
+
ovs destination add webhook "Their CRM" https://crm.example.com/in
|
|
33
|
+
ovs sync add scores_out --from lead_scores --key lead_id --to "Their CRM"
|
|
34
|
+
ovs sync run scores_out
|
|
35
|
+
|
|
36
|
+
A model's script imports its helper: `from overscore_model import rows, read, file, output, log`.
|
|
37
|
+
|
|
38
|
+
Every command prints JSON when its output is not a terminal (`{"success": true, ...}`, or
|
|
39
|
+
`{"success": false, "error": "..."}` with exit code 1), so agents can read it.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
overscore_cli/__init__.py,sha256=azGcOfKD_slXWf0XjSfYVREIObbx6HVkteFrJWlRnYo,222
|
|
2
|
+
overscore_model.py,sha256=97Dau39OyEA_QEcTfyYHVXOGsjf1lGfLEkltSrVs4kc,2835
|
|
3
|
+
overscore_cli/ovs.py,sha256=A-qiGBIVW7-Uf3tassboUzm5cEaDGeENsMXZD8Z8yuQ,102715
|
|
4
|
+
overscore_cli/skills/overscore/SKILL.md,sha256=lkjtw-QqwQSptsWZMzOdu-3aOG5iLjH7gzkOIZ-sxow,3833
|
|
5
|
+
overscore_cli/skills/overscore-model/SKILL.md,sha256=ddtKrID25Pu6JOjQd3cXFa4Uk0BhrBhXk5TCJhBotNs,3426
|
|
6
|
+
overscore_cli/skills/overscore-model/model.py,sha256=fzY8d3LdK-YOmZCvSQvA7u6wgtLFPU1si2tk7w-e5u0,1346
|
|
7
|
+
overscore_cli/skills/overscore-model/model.toml,sha256=qipT99il3l1lpIm4vIDSQw1x0-KoASIlFPWAvpBOuPU,658
|
|
8
|
+
overscore_cli/skills/overscore-sync/SKILL.md,sha256=ir2U-CvET6KP01VRBssDeBAbOcfnU0fo9lg-_lfMypE,3076
|
|
9
|
+
overscore_cli-0.1.0.dist-info/METADATA,sha256=vOU_73S-mfJ2-RKxiLFOkHbjnRkSmurpJwgRf_4_gFU,1965
|
|
10
|
+
overscore_cli-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
11
|
+
overscore_cli-0.1.0.dist-info/entry_points.txt,sha256=_yFVLipYeHJGFr1BuuP-tUlkkE4Yy2hOe-T4lTP1UF0,47
|
|
12
|
+
overscore_cli-0.1.0.dist-info/RECORD,,
|
overscore_model.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""overscore_model: the helper a Python model imports. Copied next to the script as overscore_model.py.
|
|
2
|
+
|
|
3
|
+
Standard library only; the script runs in its own interpreter, without Overscore installed.
|
|
4
|
+
|
|
5
|
+
from overscore_model import inputs, read, file, output, log
|
|
6
|
+
|
|
7
|
+
inputs() {"hubspot.contact": "/path/contact.parquet", ...}: every table the model reads, as Parquet
|
|
8
|
+
read("hubspot.contact") the path of one input (read it with pandas, polars, pyarrow or duckdb)
|
|
9
|
+
rows("hubspot.contact") the same table as a list of dicts (no libraries needed; fine up to a few hundred thousand rows)
|
|
10
|
+
file("lead_model") the local path of an upload's live version (the pickle, a lookup)
|
|
11
|
+
output(rows) the model's result: a list of dicts, or anything with .to_dict("records") (a DataFrame)
|
|
12
|
+
log("scored 120") a line in the run log
|
|
13
|
+
|
|
14
|
+
The result becomes the model's table, typed from the values. Call output once.
|
|
15
|
+
On an instant run (refresh 'instant': a landing just happened) rows()/read() of the landed table hold only the new rows,
|
|
16
|
+
the output is appended to the table, and OVERSCORE_MODE is "instant"; on a full run it is "full".
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
_OUT = os.environ.get("OVERSCORE_OUTPUT") or "output.jsonl"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _emit(obj: dict) -> None:
|
|
28
|
+
sys.stdout.write("@overscore " + json.dumps(obj, default=str) + "\n")
|
|
29
|
+
sys.stdout.flush()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def inputs() -> dict:
|
|
33
|
+
return json.loads(os.environ.get("OVERSCORE_INPUTS") or "{}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def read(name: str) -> str:
|
|
37
|
+
paths = inputs()
|
|
38
|
+
if name in paths:
|
|
39
|
+
return paths[name]
|
|
40
|
+
short = {k.split(".", 1)[-1]: v for k, v in paths.items()}
|
|
41
|
+
if name in short:
|
|
42
|
+
return short[name]
|
|
43
|
+
raise KeyError(f"{name} is not an input of this model; it reads {', '.join(paths) or 'nothing'}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def rows(name: str) -> list[dict]:
|
|
47
|
+
path = read(name)
|
|
48
|
+
side = path[:-len(".parquet")] + ".jsonl" if path.endswith(".parquet") else path + ".jsonl"
|
|
49
|
+
with open(side, encoding="utf-8") as f:
|
|
50
|
+
return [json.loads(line) for line in f if line.strip()]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def file(name: str) -> str:
|
|
54
|
+
files = json.loads(os.environ.get("OVERSCORE_FILES") or "{}")
|
|
55
|
+
if name not in files:
|
|
56
|
+
raise KeyError(f"{name} is not an upload this model asked for; it has {', '.join(files) or 'none'}")
|
|
57
|
+
return files[name]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def output(result) -> int:
|
|
61
|
+
if hasattr(result, "to_dict"):
|
|
62
|
+
result = result.to_dict("records")
|
|
63
|
+
n = 0
|
|
64
|
+
with open(_OUT, "w", encoding="utf-8") as f:
|
|
65
|
+
for r in result:
|
|
66
|
+
if not isinstance(r, dict):
|
|
67
|
+
raise TypeError("output() takes a list of dicts (or a DataFrame)")
|
|
68
|
+
f.write(json.dumps(r, default=str) + "\n")
|
|
69
|
+
n += 1
|
|
70
|
+
_emit({"output": {"rows": n}})
|
|
71
|
+
return n
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def log(line) -> None:
|
|
75
|
+
_emit({"log": str(line)})
|