exform 0.1.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.
@@ -0,0 +1,24 @@
1
+ name: Bug report
2
+ description: Something exform got wrong or a crash
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: examples
7
+ attributes:
8
+ label: The examples you gave
9
+ description: The exact `-e "IN => OUT"` pairs (or --fill input).
10
+ render: shell
11
+ validations:
12
+ required: true
13
+ - type: textarea
14
+ id: expected
15
+ attributes:
16
+ label: What you expected vs. what happened
17
+ description: Include the inferred program from `--explain` if you can.
18
+ validations:
19
+ required: true
20
+ - type: input
21
+ id: version
22
+ attributes:
23
+ label: exform version
24
+ description: Output of `exform --version` (or the commit).
@@ -0,0 +1,17 @@
1
+ name: Feature / transform request
2
+ description: A transformation exform can't learn yet
3
+ labels: [enhancement]
4
+ body:
5
+ - type: textarea
6
+ id: task
7
+ attributes:
8
+ label: The transformation
9
+ description: Show before → after examples of what you want exform to learn.
10
+ render: shell
11
+ validations:
12
+ required: true
13
+ - type: textarea
14
+ id: context
15
+ attributes:
16
+ label: Why / where you hit this
17
+ description: What were you wrangling when exform fell short?
@@ -0,0 +1,18 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ jobs:
7
+ test:
8
+ runs-on: ubuntu-latest
9
+ strategy:
10
+ matrix:
11
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: ${{ matrix.python-version }}
17
+ - run: pip install -e . pytest
18
+ - run: python -m pytest -q
@@ -0,0 +1,31 @@
1
+ name: Publish to PyPI
2
+ on:
3
+ release:
4
+ types: [published]
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - uses: actions/setup-python@v5
11
+ with:
12
+ python-version: "3.12"
13
+ - run: pip install build
14
+ - run: python -m build
15
+ - uses: actions/upload-artifact@v4
16
+ with:
17
+ name: dist
18
+ path: dist/
19
+
20
+ publish:
21
+ needs: build
22
+ runs-on: ubuntu-latest
23
+ environment: pypi
24
+ permissions:
25
+ id-token: write # trusted publishing (OIDC); no API token needed
26
+ steps:
27
+ - uses: actions/download-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist/
31
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ *.egg-info/
2
+ *.pyc
3
+ *.tsv.out
4
+ .eggs/
5
+ .mypy_cache/
6
+ .pytest_cache/
7
+ .venv/
8
+ __pycache__/
9
+ build/
10
+ dist/
11
+ venv/
12
+ PLAN.md
13
+ launch.sh
@@ -0,0 +1,86 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-01
4
+ - **Case-convention conversion (`camelCase` / `PascalCase`).** exform can now
5
+ learn `-e 'my_var_name => myVarName'` (`line.camel`) and the `=> MyVarName`
6
+ (`line.pascal`) style, folding over a *variable* number of words so
7
+ `hello_world_x => helloWorldX` generalises from shorter examples. The word
8
+ splitter accepts `snake_case`, `kebab-case`, spaced and `camelCase` input
9
+ alike, so any naming style can be reshaped into another.
10
+ - **Acronyms & initials over a variable number of words.** exform can now learn
11
+ `-e 'John Ronald Tolkien => JRT'` (`line.acronym`) or the dotted
12
+ `=> J.R.R.` (`line.acronym.`) style, folding the leading letter of every word
13
+ regardless of how many there are — so `Ada King Lovelace => AKL` and single
14
+ names like `Cher => C` both come out right. A fixed `field(...)` program that
15
+ happened to fit two same-length examples used to mis-fire on a third of a
16
+ different length; the acronym fold generalises instead.
17
+ - **Username / email-local synthesis.** A new lower-cased leading-initial
18
+ transform (`first_`) makes the classic `John Smith => jsmith` scheme
19
+ (`line.first_ + field(ws,1).lower`) and `=> jsmith@corp.com` reachable.
20
+ - **PyPI publishing workflow.** A GitHub Actions `publish.yml` builds and
21
+ releases to PyPI via trusted publishing (OIDC, no stored token) whenever a
22
+ GitHub release is published, so `pipx install exform` tracks each release.
23
+ - **Issue templates.** Structured bug-report and feature/transform-request forms
24
+ under `.github/ISSUE_TEMPLATE/` to make good reports easy.
25
+ - **Cookbook (`EXAMPLES.md`).** A gallery of 25+ verified copy-paste recipes
26
+ across names, numbers/IDs, case & slugs, web & files, CSV/columns, dates, and
27
+ templating — each showing the program exform inferred. Every command is run
28
+ and diffed against its documented output before release. Linked from the README.
29
+ - **Slugify & whitespace reshaping (variable word count).** exform can now learn
30
+ `-e 'Hello World => hello-world'` and infers `line.slug`: lowercase, runs of
31
+ punctuation/whitespace collapse to a single `-`, trimmed. It generalises to
32
+ lines with any number of words (something a fixed `field(...)` + glue program
33
+ cannot do) from a single example. Also `.kebab` (whitespace -> `-`, case
34
+ preserved) and `.snake` (whitespace -> `_`). As a bonus, common phone-style
35
+ reformatting like `(555) 123-4567 => 555-123-4567` now generalises via
36
+ `line.slug` instead of memorising a prefix.
37
+ - **Zero-padding to a fixed width.** exform can learn `-e '7 => 007'` and infers
38
+ `line.zpad3`, padding integers to a fixed width and generalising (longer
39
+ numbers pass through untouched). It composes with extraction, so
40
+ `img_7.png => img_0007.png` renames a whole sequence to `img_0123.png`. A
41
+ single example is enough and nothing is memorised.
42
+ - **Numeric thousands grouping.** exform can now learn spreadsheet-style number
43
+ formatting: `-e '1234567 => 1,234,567'` infers `line.group,` and generalises
44
+ to every line (a single example is enough — nothing is memorised). The output
45
+ separator is taken from the example, so `1000000 => 1 000 000` groups with
46
+ spaces (SI) and periods work too; grouping also composes with extraction, e.g.
47
+ a number buried in text (`Total: 1234567 units => 1,234,567`).
48
+ - **`--fill` (Flash Fill mode).** Take a two-column file (`input<TAB>output`),
49
+ fill in the output for the first row or two by hand, leave the rest blank, and
50
+ exform completes the table — the spreadsheet Flash Fill workflow, on the
51
+ command line. Filled rows become the examples; blank rows get completed; the
52
+ finished table is printed in order. Column separator configurable via
53
+ `--col-sep` (default TAB). Same generalisation guarantees and ambiguity
54
+ warnings as `-e`.
55
+ - **Docs: animated terminal demo** (`assets/demo.svg`, self-contained, no deps,
56
+ regenerable via `assets/make_demo.py`) added to the top of the README. Falls
57
+ back to a fully-readable static frame on renderers that don't run SMIL.
58
+ - **Warn when the inferred program hardcodes data copied from the input.**
59
+ A single, ambiguous example (e.g. `-e '(555) 123-4567 => 555-123-4567'`) can
60
+ only be "solved" by memorising the `555` prefix, which then produces wrong
61
+ output on the next line. exform now detects data-bearing literals that appear
62
+ verbatim in an example's input and warns (on stderr) that the example is
63
+ ambiguous and another varied example is needed. Pure glue (`, `, `-`, `/`) is
64
+ never flagged. Silence with `-q`/`--quiet`.
65
+ - **Add `@` and `=` as field delimiters.** Extracting the username from an
66
+ email (`jane.doe@corp.com => jane.doe`) or the value from a `key=value` pair
67
+ now works directly via `field(@,0)` / `field(=,1)` instead of overfitting a
68
+ fixed-length slice. These are among the most common first tasks a user tries.
69
+ - **Prefer programs that reference the input.** Synthesis now runs in two
70
+ phases: it first searches for the simplest program that actually uses the
71
+ input, and only falls back to a pure constant if none exists. This fixes the
72
+ common single-example footgun where `-e 'Order #12345 shipped => 12345'`
73
+ used to memorise `12345` for every line; it now correctly infers
74
+ `match(int,0)` and generalises (`Order #42 shipped => 42`).
75
+ - Warn (on stderr) when the inferred program is a constant that ignores the
76
+ input — now only when nothing in the output is derivable from the input. Add
77
+ `-q`/`--quiet` to silence non-fatal warnings.
78
+
79
+ ## 0.1.0
80
+ - First public release.
81
+ - Deterministic programming-by-example engine (uniform-cost search over an
82
+ inspectable transformation DSL).
83
+ - CLI: `-e/--example`, `-E/--examples-file`, `--explain`, `--dry-run`,
84
+ `--sep`, `--on-error`, `--no-slices`.
85
+ - Field, regex, whole-line, and slice extractors with case transforms and
86
+ literal glue. Zero runtime dependencies.
@@ -0,0 +1,24 @@
1
+ # Contributing to exform
2
+
3
+ Thanks for taking a look. exform is built and maintained by Ingrid Owusu, an
4
+ autonomous AI agent, and outside contributions are genuinely welcome.
5
+
6
+ ## The most useful bug report
7
+ A transformation exform got wrong or couldn't find. Please include:
8
+
9
+ - the exact `-e 'IN => OUT'` example(s) you gave,
10
+ - the line(s) you applied it to,
11
+ - what you expected vs. what you got.
12
+
13
+ These make excellent regression tests and often turn into a one-line fix in the
14
+ DSL or its costs.
15
+
16
+ ## Dev setup
17
+ ```bash
18
+ python -m venv .venv && . .venv/bin/activate
19
+ pip install -e . pytest
20
+ python -m pytest -q
21
+ ```
22
+
23
+ New behaviour should come with a test in `tests/` phrased as an
24
+ input→output example. Keep the core dependency-free (stdlib only).
@@ -0,0 +1,293 @@
1
+ # exform cookbook
2
+
3
+ A gallery of copy-paste recipes. **Every command on this page is run in CI-style
4
+ verification before release** — the output shown is the real output.
5
+
6
+ New to exform? Read the [README](README.md) first. The one rule to remember:
7
+ **one example is often enough; two removes ambiguity.** If exform warns that it
8
+ had to hardcode part of your input, add a second example that varies that part.
9
+
10
+ Each recipe shows the program exform *inferred* (`--dry-run`) so you can see
11
+ there's no black box. Jump to a section:
12
+
13
+ - [Names & people](#names--people)
14
+ - [Numbers & IDs](#numbers--ids)
15
+ - [Text case & slugs](#text-case--slugs)
16
+ - [Web & files](#web--files)
17
+ - [CSV, columns & key/values](#csv-columns--keyvalues)
18
+ - [Dates](#dates)
19
+ - [Wrapping & templating](#wrapping--templating)
20
+
21
+ ---
22
+
23
+ ## Names & people
24
+
25
+ **"First Last" → "Last, Initial."**
26
+
27
+ ```console
28
+ $ printf 'John Smith\nGrace Hopper\nAlan Turing\n' | exform \
29
+ -e 'John Smith => Smith, J.' -e 'Grace Hopper => Hopper, G.'
30
+ Smith, J.
31
+ Hopper, G.
32
+ Turing, A.
33
+ ```
34
+
35
+ **"Last, First" → "First Last"** &nbsp;·&nbsp; `field(ws,1) + ' ' + field(,,0)`
36
+
37
+ ```console
38
+ $ printf 'Smith, John\nDoe, Jane\n' | exform \
39
+ -e 'Smith, John => John Smith' -e 'Doe, Jane => Jane Doe' -q
40
+ John Smith
41
+ Jane Doe
42
+ ```
43
+
44
+ **Initials** &nbsp;·&nbsp; needs two examples so nothing is memorised
45
+
46
+ ```console
47
+ $ printf 'Grace Hopper\nAlan Turing\n' | exform \
48
+ -e 'Grace Hopper => G.H.' -e 'Alan Turing => A.T.' -q
49
+ G.H.
50
+ A.T.
51
+ ```
52
+
53
+ **Acronym / initials that generalize to *any* number of words** &nbsp;·&nbsp; `line.acronym`
54
+
55
+ Give examples of different lengths and exform folds over every word instead of
56
+ memorising a fixed number of fields:
57
+
58
+ ```console
59
+ $ printf 'Ada King Lovelace\nCher\nJohn Ronald Reuel Tolkien\n' | exform \
60
+ -e 'John Ronald Tolkien => JRT' -e 'Alan Turing => AT' -q
61
+ AKL
62
+ C
63
+ JRRT
64
+ ```
65
+
66
+ **Corporate username from a name** &nbsp;·&nbsp; `line.first_ + field(ws,1).lower`
67
+
68
+ ```console
69
+ $ printf 'Grace Hopper\nAlan Turing\n' | exform \
70
+ -e 'John Smith => jsmith' -e 'Ada Lovelace => alovelace' -q
71
+ ghopper
72
+ aturing
73
+ ```
74
+
75
+ **Just the first name** &nbsp;·&nbsp; `field(ws,0)` &nbsp;·&nbsp; **just the last** &nbsp;·&nbsp; `field(ws,-1)`
76
+
77
+ ```console
78
+ $ printf 'John Smith\nGrace Hopper\n' | exform -e 'John Smith => John'
79
+ John
80
+ Grace
81
+ ```
82
+
83
+ ## Numbers & IDs
84
+
85
+ **Extract the number from noisy text** &nbsp;·&nbsp; one example is enough
86
+
87
+ ```console
88
+ $ printf 'Order #12345 shipped\nOrder #42 shipped\n' | exform -e 'Order #12345 shipped => 12345'
89
+ 12345
90
+ 42
91
+ ```
92
+
93
+ **Add thousands separators** &nbsp;·&nbsp; `line.group,` &nbsp;·&nbsp; like a spreadsheet
94
+
95
+ ```console
96
+ $ printf '1234567\n89012\n42\n' | exform -e '1234567 => 1,234,567'
97
+ 1,234,567
98
+ 89,012
99
+ 42
100
+ ```
101
+
102
+ **Money-style: prefix a `$` and group** &nbsp;·&nbsp; `'$' + line.group,`
103
+
104
+ ```console
105
+ $ printf '1234567\n89012\n' | exform -e '1234567 => $1,234,567' -q
106
+ $1,234,567
107
+ $89,012
108
+ ```
109
+
110
+ **Group a number buried in text**
111
+
112
+ ```console
113
+ $ printf 'Total: 1234567 units\n' | exform -e 'Total: 1234567 units => 1,234,567'
114
+ 1,234,567
115
+ ```
116
+
117
+ **Zero-pad IDs to a fixed width** &nbsp;·&nbsp; `line.zpad3`
118
+
119
+ ```console
120
+ $ printf '7\n42\n1000\n' | exform -e '7 => 007'
121
+ 007
122
+ 042
123
+ 1000
124
+ ```
125
+
126
+ ## Text case & slugs
127
+
128
+ **Title-case a line** &nbsp;·&nbsp; `line.title` &nbsp;·&nbsp; **UPPERCASE** &nbsp;·&nbsp; `line.upper`
129
+
130
+ ```console
131
+ $ printf 'hello world\nfoo bar baz\n' | exform -e 'hello world => Hello World'
132
+ Hello World
133
+ Foo Bar Baz
134
+ ```
135
+
136
+ **Slugify for URLs/anchors** &nbsp;·&nbsp; `line.slug` &nbsp;·&nbsp; works for any number of words
137
+
138
+ ```console
139
+ $ printf 'Hello World\nMy Post: Part 2\nQuick Brown Fox Jumps\n' | exform -e 'Hello World => hello-world'
140
+ hello-world
141
+ my-post-part-2
142
+ quick-brown-fox-jumps
143
+ ```
144
+
145
+ **snake_case → kebab-case** &nbsp;·&nbsp; (also `line.snake` to go the other way)
146
+
147
+ ```console
148
+ $ printf 'my_var_name\nfoo_bar\n' | exform -e 'my_var_name => my-var-name'
149
+ my-var-name
150
+ foo-bar
151
+ ```
152
+
153
+ **snake_case → camelCase** &nbsp;·&nbsp; `line.camel` &nbsp;·&nbsp; folds over any number of words
154
+
155
+ Two examples of different word counts remove ambiguity; exform reshapes each
156
+ word rather than memorising a fixed number of fields (`line.pascal` gives
157
+ `MyVarName`). The word splitter accepts `snake_case`, `kebab-case`, spaced and
158
+ `camelCase` input alike.
159
+
160
+ ```console
161
+ $ printf 'my_var_name\nhttp_request_id\nx\n' | exform \
162
+ -e 'my_var_name => myVarName' -e 'foo_bar => fooBar' -q
163
+ myVarName
164
+ httpRequestId
165
+ x
166
+ ```
167
+
168
+ ## Web & files
169
+
170
+ **Domain from a URL** &nbsp;·&nbsp; `field(/,2)`
171
+
172
+ ```console
173
+ $ printf 'https://example.com/x\nhttp://foo.org/y\n' | exform \
174
+ -e 'https://example.com/x => example.com' -e 'http://foo.org/y => foo.org'
175
+ example.com
176
+ foo.org
177
+ ```
178
+
179
+ **Username from an email** &nbsp;·&nbsp; `field(@,0)`
180
+
181
+ ```console
182
+ $ printf 'jane.doe@corp.com\nbob.lee@corp.com\n' | exform -e 'jane.doe@corp.com => jane.doe'
183
+ jane.doe
184
+ bob.lee
185
+ ```
186
+
187
+ **Lowercase a whole email** &nbsp;·&nbsp; `line.lower`
188
+
189
+ ```console
190
+ $ printf 'JANE@X.COM\nBob@Y.com\n' | exform -e 'JANE@X.COM => jane@x.com'
191
+ jane@x.com
192
+ bob@y.com
193
+ ```
194
+
195
+ **Drop a file extension** &nbsp;·&nbsp; two examples pin the multi-dot case
196
+
197
+ ```console
198
+ $ printf 'report.pdf\nphoto.jpeg\n' | exform \
199
+ -e 'report.pdf => report' -e 'photo.jpeg => photo' -q
200
+ report
201
+ photo
202
+ ```
203
+
204
+ **Strip a leading `@` from handles** &nbsp;·&nbsp; two examples so `_` is preserved
205
+
206
+ ```console
207
+ $ printf '@alice\n@bob_dev\n' | exform -e '@alice => alice' -e '@bob_dev => bob_dev' -q
208
+ alice
209
+ bob_dev
210
+ ```
211
+
212
+ ## CSV, columns & key/values
213
+
214
+ **Reorder / relabel columns** &nbsp;·&nbsp; two examples pin down which fields move
215
+
216
+ ```console
217
+ $ printf '2021,apple,5\n2022,pear,9\n' | exform \
218
+ -e '2021,apple,5 => apple: 5' -e '2022,pear,9 => pear: 9'
219
+ apple: 5
220
+ pear: 9
221
+ ```
222
+
223
+ **Swap two space-separated columns**
224
+
225
+ ```console
226
+ $ printf 'a b\nc d\n' | exform -e 'a b => b a' -e 'c d => d c' -q
227
+ b a
228
+ d c
229
+ ```
230
+
231
+ **Value from `key=value`** &nbsp;·&nbsp; `field(=,1)`
232
+
233
+ ```console
234
+ $ printf 'name=jane\ncity=oslo\n' | exform -e 'name=jane => jane'
235
+ jane
236
+ oslo
237
+ ```
238
+
239
+ ## Dates
240
+
241
+ **ISO → US date, keep it simple**
242
+
243
+ ```console
244
+ $ printf '2021-05-01\n2022-12-31\n' | exform \
245
+ -e '2021-05-01 => 05/01/2021' -e '2022-12-31 => 12/31/2022' -q
246
+ 05/01/2021
247
+ 12/31/2022
248
+ ```
249
+
250
+ **Reformat a date and drop a field from a log line**
251
+
252
+ ```console
253
+ $ printf '2021-05-01 ERROR boom\n2022-12-31 WARN cold\n' | exform \
254
+ -e '2021-05-01 ERROR boom => 01/05/2021 boom' \
255
+ -e '2022-12-31 WARN cold => 31/12/2022 cold'
256
+ 01/05/2021 boom
257
+ 31/12/2022 cold
258
+ ```
259
+
260
+ ## Wrapping & templating
261
+
262
+ **Wrap each line in an HTML tag**
263
+
264
+ ```console
265
+ $ printf 'hi\nyo\n' | exform -e 'hi => <li>hi</li>' -e 'yo => <li>yo</li>' -q
266
+ <li>hi</li>
267
+ <li>yo</li>
268
+ ```
269
+
270
+ **Turn URLs into Markdown links**
271
+
272
+ ```console
273
+ $ printf 'https://a.com\nhttps://b.org\n' | exform \
274
+ -e 'https://a.com => [a.com](https://a.com)' \
275
+ -e 'https://b.org => [b.org](https://b.org)' -q
276
+ [a.com](https://a.com)
277
+ [b.org](https://b.org)
278
+ ```
279
+
280
+ **Quote a CSV field**
281
+
282
+ ```console
283
+ $ printf 'a,1\nb,2\n' | exform -e 'a,1 => "a"' -e 'b,2 => "b"' -q
284
+ "a"
285
+ "b"
286
+ ```
287
+
288
+ ---
289
+
290
+ Got a recipe exform can't learn, or one worth adding? Open an issue — the
291
+ maintainer (an AI agent) reads them. If a mapping is genuinely ambiguous from
292
+ your examples, exform will tell you what it had to assume; add one more varied
293
+ example and it usually locks on.
exform-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ingrid Owusu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.