New building permits from San Francisco's DataSF open data API (public domain, PDDL 1.0): address, parcel, work description, permit type, status, dates, estimated cost, uses, units and map point. Keyword, cost and ZIP filters, dedupe across runs. Unofficial.
Unreleased: every invalid input is a free row (1104g, local, not built or pushed; based on cloud build 0.2.5)
Change: every input rejected by parse_input (for example "abc" for a whole number, an unknown mode, a bad ZIP code or date) used to fail the run through Actor.fail (exit 1, FAILED). It now ends like the 1104f wrong-type path: exit 0, one free status row (statusRow: true, status: error, charged: false), no RUN_SUMMARY, nothing fetched or charged, and the row's message is also the terminal status message: Invalid input, nothing was fetched or charged: <reason>. Each <reason> is the same text as before.
Unchanged: upstream, network and runtime failures; valid input; pricing and input schema.
Tests: tests/test_invalid_input.py gains an end-to-end test that runs the Actor offline on former exit-1 inputs and checks exit 0, one free row, the message and no RUN_SUMMARY.
Unreleased: invalid input no longer crashes (2026-11-04, local, not built or pushed; based on cloud build 0.2.4)
Fix: input that is not a JSON object (a string, number, null or list), and an infinite lookbackDays, minEstimatedCost or maxItems, or a zipCodes that is a number or boolean instead of a list or string, crashed the run with exit 91 before anything was fetched. It now ends with exit 0 and one free status row (statusRow: true, charged: false, no RUN_SUMMARY): Invalid input, nothing was fetched or charged: <reason>.
Unchanged: every other invalid input still fails the run through Actor.fail (exit 1) with the same message.
Memory: defaultMemoryMbytes and defaultRunOptions.memoryMbytes are now 256 (were 1024), minMemoryMbytes 128 (was 256); max 2048 unchanged. The README names no memory size and already says there is no start fee.
New unit tests: tests/test_invalid_input.py; tests/test_pricing.py now expects the 256 MB default memory.
2026-11-02 (actor.json version unchanged: 0.2)
Default max results lowered from 5 to 4 permits, so {} and the prefill cost at most $0.008 (5 permits were exactly $0.01, the Store test limit, with no margin). The Console prefill now sets minEstimatedCost to 50000, so the sample run shows real projects instead of $1 permits.
Behaviour changes:
maxItems schema default, prefill and code fallback (DEFAULT_MAX_ITEMS, used for omitted, null or blank maxItems) 5 to 4.
New minEstimatedCost prefill 50000 (Console form only; it is not a default, so {} still has no cost filter).
exampleRunInput now equals the prefill exactly: {"minEstimatedCost": 50000, "maxItems": 4}. Before it was {"mode": "daily_feed", "lookbackDays": 7, "minEstimatedCost": 50000, "maxItems": 5}; mode and lookbackDays were the defaults, so only maxItems changes what the run does.
Files changed:
my_actor/main.py: DEFAULT_MAX_ITEMS 5 to 4, with its comment.
.actor/input_schema.json: maxItems default and prefill 5 to 4, description "Default 4 (at most $0.008)"; minEstimatedCost prefill 50000.
.actor/actor.json: exampleRunInput as above. The version is still 0.2.
pricing.json: the _note and emptyInputAutoTest text say 4 permits = $0.008.
README.md and the identical README.proposed.md: input table (minEstimatedCost notes the 50000 prefill; maxItems default 4) and the pricing line (4 permits, at most $0.008).
2026-10-30, 10:07 PM EDT (actor.json version unchanged: 0.2)
Default max results lowered from 100 to 5 permits (publisher approved), so {} and the prefill cost at most $0.01. The README licence URL now matches the code.
Files changed:
my_actor/main.py: DEFAULT_MAX_ITEMS 100 to 5, with its comment.
.actor/input_schema.json: maxItems default and prefill 100 to 5; the description says "Default 5 (at most $0.01)".
.actor/actor.json: exampleRunInput.maxItems 100 to 5. The version is still 0.2.
pricing.json: the _note and emptyInputAutoTest text now say 5 permits = $0.01.
tests/test_pricing.py: test_readme_price requires 5 permits, $0.01 and the 5 in the maxItems row, and rejects the old "at most 100 permits" and "$0.20". The wrong-price check is kept. The default test requires default == prefill == 5, a $0.01 cost and exampleRunInput maxItems 5.
tests/test_input.py: the {} and blank maxItems defaults are now 5.
tests/test_main.py: test_empty_input_is_100_sf_permits is renamed test_empty_input_is_5_sf_permits. test_dedupe_across_runs and test_in_run_duplicates_skipped now pass maxItems 100 explicitly so they still test dedupe instead of the cap.
CHANGELOG.md: this entry.
Note: TEST_REPORT.md and NOTES.md still record the earlier 100-permit run as a historical measurement.
2026-10-30, later the same evening (actor.json version unchanged: 0.2)
tests/test_pricing.py::test_readme_price now matches the publisher's reviewed README.md as it is (README.md not
edited). It checks only facts the README must state, derived from the code and pricing.json: the
per-permit price ($0.002), the per-1,000 price ($2), "no start fee", and the most {} can cost (at most
DEFAULT_MAX_ITEMS = 100 permits, $0.20). Markdown emphasis is ignored, the word "ESTIMATED" is no longer required, and any other per-unit price
in the README fails the test. The suite passes fully again (the test had failed since the publisher's README of
2026-10-30 21:46 ET).
Fixed licenceUrl on every record: http://opendatacommons.org/licenses/pddl/1.0/ (the termsLink in DataSF's
dataset metadata) answers HTTP 500; it is now the licence's canonical page
https://opendatacommons.org/licenses/pddl/1-0/ (curl on 2026-10-30 about 9:55 PM ET: HTTP 200, no redirect; the
old URL: HTTP 500).
Files changed:
my_actor/sources.py: licenceUrl of San Francisco.
sample_output.json: the 3 sample records' licenceUrl.
tests/test_sources.py: asserts the new URL on built records, in CITIES and in sample_output.json.
tests/record.schema.json: licenceUrl must start with https://.
CHANGELOG.md: this entry.
NOT changed: NOTES.md line 33 (quotes DataSF's metadata as fetched, which still has the old link) and README.md.
0.2 (2026-10-29, local only, not published)
San Francisco only (Ranger, 10:12 EDT): Chicago removed entirely because it has no named open licence and its terms carry a termination clause. Removed from sources.py, the input schema (the cities field is gone), tests and fixtures, README, NOTES and sample_output. Saved inputs that still pass cities: ["san-francisco"] keep working; any other city fails with a clear error.
Output fields that only applied to Chicago (workType, totalFee, businessContacts, contactsWithheld) and the contact filter code were removed.
Title and description now name San Francisco only.
README: note that business or contractor names can contain individuals' names, and the FCRA/TCPA/CAN-SPAM line copied from florida-sunbiz-daily-filings' README.
Pricing unchanged: permit at $0.002 (ESTIMATED); {} measured at 100 permits = $0.200.
0.1 (2026-10-29, local only, not published)
First version: San Francisco (DataSF, PDDL 1.0) and Chicago building permits via Socrata SODA /resource/; normalised schema; issued/filed date basis; keyword, minimum cost and ZIP filters; fair per-city split; dedupe state; no owner or person names (Chicago contacts kept only for businesses with a legal form in non-owner roles).
Pricing: one event only (permit, $0.002, ESTIMATED), no start fee, no $0 events. {} and the prefill are capped at 100 permits ($0.20). Blank maxItems gives the default 100.