# Changelog of San Francisco Building Permits Feed: DataSF Open Data (PDDL) (`fecklesspleb/city-building-permits-feed`) Actor

- **URL**: https://apify.com/fecklesspleb/city-building-permits-feed/changelog.md
- **Full Actor documentation**: https://apify.com/fecklesspleb/city-building-permits-feed.md

## Changelog

### 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.
- `requirements.txt`: `apify` pinned to `>= 4.1.0, < 4.2.0` (was `>= 4.0.0, < 5.0.0`).
- 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).
- Tests: tests/test_input.py (`test_empty_input_defaults`, `test_blank_max_items_is_default`), tests/test_main.py (`test_empty_input_is_5_sf_permits` renamed `test_empty_input_is_4_sf_permits`; comment in `test_in_run_duplicates_skipped`), tests/test_pricing.py (`test_readme_price`, `test_small_default_run_under_25_cents`).

### 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.
- README.md: line 35 (`maxItems` default 5), line 77 (`licenceUrl` https://opendatacommons.org/licenses/pddl/1-0/) and line 96 (5 permits, $0.01). Nothing else changed.
- 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).
- Files changed: `tests/test_pricing.py` (that test, a `_plain()` helper, module docstring), `CHANGELOG.md`.

### 2026-10-30 (actor.json version unchanged: 0.2)

- 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.
