filemaker-patchwork 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.
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: filemaker-patchwork
3
+ Version: 0.1.0
4
+ Summary: Let your AI agent edit FileMaker: fields, scripts, value lists, layouts and themes, checked and reversible. MCP server for Claude Code, Cursor, Codex and Antigravity.
5
+ Author: Abdeltoto
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Abdeltoto/patchwork-filemaker-mcp
8
+ Project-URL: Issues, https://github.com/Abdeltoto/patchwork-filemaker-mcp/issues
9
+ Project-URL: Changelog, https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/CHANGELOG.md
10
+ Keywords: filemaker,claris,fmupgradetool,fmp12,mcp,mcp-server,ai-agents,claude-code,cursor,codex,antigravity,low-code
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Software Development :: Build Tools
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: image
24
+ Requires-Dist: Pillow>=9; extra == "image"
25
+ Dynamic: license-file
26
+
27
+ <!-- mcp-name: io.github.Abdeltoto/patchwork -->
28
+ <div align="center">
29
+
30
+ <img src="https://raw.githubusercontent.com/Abdeltoto/patchwork-filemaker-mcp/main/docs/images/banner.png" alt="Patchwork: let your AI agent edit FileMaker" width="100%">
31
+
32
+ <br>
33
+
34
+ [![CI](https://img.shields.io/github/actions/workflow/status/Abdeltoto/patchwork-filemaker-mcp/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/Abdeltoto/patchwork-filemaker-mcp/actions/workflows/ci.yml)
35
+ [![Stars](https://img.shields.io/github/stars/Abdeltoto/patchwork-filemaker-mcp?style=flat-square&color=8b5cf6)](https://github.com/Abdeltoto/patchwork-filemaker-mcp/stargazers)
36
+ [![License: MIT](https://img.shields.io/badge/license-MIT-14b8a6?style=flat-square)](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/LICENSE)
37
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776AB?style=flat-square&logo=python&logoColor=white)
38
+ ![FileMaker 2026](https://img.shields.io/badge/FileMaker-2026-000000?style=flat-square&logo=claris&logoColor=white)
39
+ ![MCP server](https://img.shields.io/badge/MCP-server-000000?style=flat-square&logo=modelcontextprotocol&logoColor=white)
40
+ ![Zero dependencies](https://img.shields.io/badge/dependencies-0-f59e0b?style=flat-square)
41
+ [![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4?style=flat-square)](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/CONTRIBUTING.md)
42
+
43
+ <a href="#connect-your-agent"><img src="https://img.shields.io/badge/Claude_Code-191919?style=for-the-badge&logo=claude&logoColor=D97757" alt="Claude Code"></a>
44
+ <a href="#connect-your-agent"><img src="https://img.shields.io/badge/Cursor-000000?style=for-the-badge&logo=cursor&logoColor=white" alt="Cursor"></a>
45
+ <a href="#connect-your-agent"><img src="https://img.shields.io/badge/Codex-000000?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3QgeD0iMS41IiB5PSIzIiB3aWR0aD0iMjEiIGhlaWdodD0iMTgiIHJ4PSI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIvPjxwYXRoIGQ9Ik02LjUgOWwzLjUgMy0zLjUgMyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xMi41IDE1LjVoNSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==" alt="Codex"></a>
46
+ <a href="#connect-your-agent"><img src="https://img.shields.io/badge/Antigravity-1a1a2e?style=for-the-badge&logo=google&logoColor=4285F4" alt="Google Antigravity"></a>
47
+
48
+ **[Quick start](#quick-start)** · **[Examples](#see-it-in-action)** · **[Connect your agent](#connect-your-agent)** · **[What it can change](#what-it-can-change)** · **[Field guide](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/docs/fmupgradetool-field-guide.md)** · **[Français](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/README.fr.md)**
49
+
50
+ </div>
51
+
52
+ <br>
53
+
54
+ > **Friday, 4 PM. A client asks for "just one more field" on the detail layout.**
55
+ > You know the drill: define the field, copy the label, nudge every object below it, stretch the part… then do it all again on the next layout.
56
+ >
57
+ > **Patchwork lets your AI agent do those clicks for you.** Just ask, in your own words, the way you would ask a colleague. Your agent adds the field and places it right where you said, writes scripts, creates value lists and restyles layouts, in a real `.fmp12` file. Every change is checked, every change can be undone, and your original file is never touched.
58
+
59
+ Free and open source. Works with Claude Code, Cursor, Codex and Antigravity.
60
+
61
+ <p align="center"><a href="https://youtu.be/AkFhBpB0YYY"><img src="https://raw.githubusercontent.com/Abdeltoto/patchwork-filemaker-mcp/main/docs/images/video-card.jpg" alt="Watch the 90-second Patchwork intro on YouTube" width="80%"></a><br><sub>▶ <a href="https://youtu.be/AkFhBpB0YYY">Watch the 90-second intro on YouTube</a> · <a href="https://youtu.be/g5UkJLiYAhA">Version française</a></sub></p>
62
+
63
+ ## Quick start
64
+
65
+ Three commands, then you just talk to your agent.
66
+
67
+ ```bash
68
+ pip install filemaker-patchwork
69
+ patchwork doctor # finds the Claris command-line tools
70
+ patchwork init . # wires this folder for Claude Code, Cursor, Codex and Antigravity
71
+ ```
72
+
73
+ No install with [uv](https://docs.astral.sh/uv/): `uvx filemaker-patchwork doctor`, and `uvx filemaker-patchwork mcp` as the MCP server command.
74
+
75
+ Then ask your agent: *"Open Invoice.fmp12 and add a Status field to Clients."* That's it.
76
+
77
+ ### Good to know
78
+
79
+ - **It changes structure, not records.** Fields, scripts, layouts, value lists, themes. For your data, use the Data API or OData.
80
+ - **It works on closed files, not hosted ones.** Edit a copy, test it, then deploy it as you usually do.
81
+ - **It needs the Claris command-line tools** (FMUpgradeTool and FMDeveloperTool). They come with the FileMaker Server installer, the free trial works, and no server has to run: [install the Claris tools](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/docs/install-fmupgradetool.md). Patchwork itself has no other dependency.
82
+
83
+ ## See it in action
84
+
85
+ Four real sessions on the free [Claris Invoice solution](https://marketplace.claris.com/detail/2000.html). For each one: what was asked, what the agent did, and the file before and after. No mockups.
86
+
87
+ <p align="center"><img src="https://raw.githubusercontent.com/Abdeltoto/patchwork-filemaker-mcp/main/docs/images/example-schema.png" alt="Prompt: add a Loyalty Tier field and a value list. The field and the value list appear in the file." width="100%"></p>
88
+
89
+ <p align="center"><img src="https://raw.githubusercontent.com/Abdeltoto/patchwork-filemaker-mcp/main/docs/images/example-script.png" alt="Prompt: write a Say Hello script. The agent copies a real dialog step and creates a two-step script." width="100%"></p>
90
+
91
+ <p align="center"><img src="https://raw.githubusercontent.com/Abdeltoto/patchwork-filemaker-mcp/main/docs/images/example-cleanup.png" alt="Prompt: remove the Dev layouts. Six layouts are deleted and the agent warns about three scripts that pointed at them." width="100%"></p>
92
+
93
+ <p align="center"><img src="https://raw.githubusercontent.com/Abdeltoto/patchwork-filemaker-mcp/main/docs/images/example-rebrand.png" alt="Prompt: rebrand for Maple and Oak. The blue Claris header becomes a forest green Maple and Oak header." width="100%"></p>
94
+
95
+ ## How it works
96
+
97
+ <p align="center"><img src="https://raw.githubusercontent.com/Abdeltoto/patchwork-filemaker-mcp/main/docs/images/how-it-works.jpg" alt="An AI agent stitches patches onto a FileMaker file, with a verified check and an undo arrow" width="80%"></p>
98
+
99
+ Why can't your agent just do this on its own? A FileMaker file is a binary file, and its structure hides behind dialogs an agent cannot click. Claris does ship a command-line way in, [FMUpgradeTool](https://help.claris.com/en/app-upgrade-tool-guide/content/index.html), but it is picky: every object is addressed by a UUID, and a patch that is slightly wrong is **silently skipped**, while the tool still prints `Patch File Applied`.
100
+
101
+ Patchwork is the careful assistant in between:
102
+
103
+ | | |
104
+ |:-:|---|
105
+ | 🧵 **Writes the patch** | Reads the file's XML export and builds a patch with the right UUIDs, ids and catalog structure, so your agent doesn't have to |
106
+ | 🔍 **Checks the result** | Re-exports the file after every change and compares it with the previous version. If FileMaker skipped something, the change is refused, not silently lost |
107
+ | 🧭 **Warns before breaking** | Before a delete, it lists the scripts, layouts and calculations that use the object, and waits for your OK |
108
+ | 🕐 **Keeps every version** | Each change is a numbered version with its diff. Changed your mind? `undo` always works |
109
+ | 🔒 **Protects the original** | Everything happens on a working copy. `save` writes a new file and never overwrites yours |
110
+
111
+ More in [how it works](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/docs/how-it-works.md).
112
+
113
+ ## Connect your agent
114
+
115
+ One command sets everything up. Run `patchwork init .` in your project folder, new or existing: it adds the rules, the skills and the MCP configuration for every agent, and keeps the files you already have.
116
+
117
+ | Agent | Configuration written by `init` | Then |
118
+ |---|---|---|
119
+ | <img src="https://cdn.simpleicons.org/claude/D97757" width="16"> **Claude Code** | `.mcp.json` · `CLAUDE.md` · `.claude/skills/` | Run `claude` in the folder and approve `patchwork` |
120
+ | <img src="https://cdn.simpleicons.org/cursor/808080" width="16"> **Cursor** | `.cursor/mcp.json` · `.cursor/skills/` | Enable `patchwork` in Settings › MCP |
121
+ | ⌨️ **Codex** | `.codex/config.toml` · `AGENTS.md` · `.agents/skills/` | Trust the project when Codex asks |
122
+ | <img src="https://cdn.simpleicons.org/google/4285F4" width="16"> **Antigravity** | `.agents/mcp_config.json` · `AGENTS.md` · `.agents/skills/` | Open the folder as a workspace |
123
+
124
+ Every agent gets the same 21 tools through `patchwork mcp`, a stdio MCP server. Your file's password stays yours: set it once in your shell with `PATCHWORK_PASSWORD`, and agents never see it. Manual setup and the full tool list: [AI agents](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/docs/ai-agents.md).
125
+
126
+ ## What it can change
127
+
128
+ Fields, plain-text scripts, value lists, colors and fonts, even a full rebrand. Every operation below was tried on real files and confirmed by re-exporting them.
129
+
130
+ | | Add | Edit | Delete |
131
+ |---|:-:|:-:|:-:|
132
+ | **Fields** · text, number, date, time, timestamp, container, calculation, global | ✅ | | ✅ |
133
+ | **Scripts**, read and written as FileMaker text | ✅ | ✅ | ✅ |
134
+ | **Value lists** | ✅ | ✅ | ✅ |
135
+ | **Custom functions** | ✅ | ✅ | ✅ |
136
+ | **Layouts** · place a field with its label, whole layout, colors, fonts, swap a field | ✅ field | ✅ | ✅ |
137
+ | **Themes** · colors, fonts, name | | ✅ | |
138
+ | **Tables**, table occurrences | 👁 read | | |
139
+
140
+ > [!TIP]
141
+ > Scripts read and write the way the Script Workspace shows them, one step per line:
142
+ > `Set Field [ Clients::Status ; "Gold" ]`. Edit the text of `patchwork show script NAME` and send it back with `set-steps`: untouched lines keep their exact step, and every step is re-read after the change, so a dropped parameter is caught.
143
+
144
+ What FMUpgradeTool cannot do (record data, a brand-new theme, a single layout object on its own) is listed in [the field guide](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/docs/fmupgradetool-field-guide.md).
145
+
146
+ ## Use it without an agent
147
+
148
+ Prefer the terminal? Everything the agent does is a plain command you can run yourself:
149
+
150
+ ```bash
151
+ patchwork open "Invoice.fmp12" --account Admin # working copy, version v000
152
+ patchwork add-field Clients "Loyalty Tier"
153
+ patchwork add-value-list "Loyalty Tiers" Bronze Silver Gold
154
+ patchwork log # every version and what changed
155
+ patchwork undo
156
+ patchwork save "Invoice v2.fmp12"
157
+ ```
158
+
159
+ <details>
160
+ <summary><b>All commands</b></summary>
161
+
162
+ <br>
163
+
164
+ | Command | What it does |
165
+ |---|---|
166
+ | `open SRC --account NAME` | Starts a working copy in `.patchwork/` (version v000). The source is never modified |
167
+ | `status`, `log` | Current version, and every version with what changed |
168
+ | `ls [KIND] [TABLE]` | Lists layouts, scripts, value lists, custom functions, themes, tables, fields |
169
+ | `show KIND NAME` | Prints a script as FileMaker text (`--xml` for XML), anything else as XML. `show step NAME` prints a real example of a step |
170
+ | `add-field TABLE NAME` | Adds a field: `--type`, `--calc`, `--global`, `--image logo.png` |
171
+ | `add-value-list NAME VALUES...` | Adds a custom value list |
172
+ | `add-custom-function NAME --calc` | Adds a custom function, with `--param` |
173
+ | `add-script NAME STEPS.txt` | Creates a script from FileMaker script text (or `<Step>` XML) |
174
+ | `set-steps`, `append-steps` | Rewrites a script with edited text from `show`, or adds steps at the end |
175
+ | `place-field LAYOUT TO::FIELD --near FIELD` | Puts a field on a layout below another one, with a copy of its look and label. `--side right`, `--label` |
176
+ | `replace KIND NAME FILE.xml` | Replaces an object with your edited version of `show` |
177
+ | `uses KIND NAME` | Lists what references an object, and where its name appears in calculations |
178
+ | `delete KIND NAME` | Deletes a script, layout, value list, custom function or field. Refused while other objects reference it, unless `--force` |
179
+ | `retheme`, `recolor-layouts` | Rewrites colors and fonts of a theme, or of the layouts' local styles |
180
+ | `push PATCH.xml` | Applies a hand-written FMUpgradeTool patch, with the same checks |
181
+ | `undo` | Drops the last version |
182
+ | `save DEST` | Writes the current version to a new `.fmp12` |
183
+ | `rebrand SRC DEST --brand brand.json` | One shot: colors, fonts, logo and theme name on a whole solution |
184
+ | `mcp` | The MCP server for agents |
185
+ | `init DIR` | Wires a folder for Claude Code, Cursor, Codex and Antigravity |
186
+ | `doctor` | Shows which Claris tools Patchwork found |
187
+
188
+ Every editing command takes `-m "note"` for the log and `--dry-run` to write the patch without applying it. More in [examples and recipes](https://github.com/Abdeltoto/patchwork-filemaker-mcp/tree/main/examples).
189
+
190
+ </details>
191
+
192
+ ## The FMUpgradeTool field guide
193
+
194
+ The [field guide](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/docs/fmupgradetool-field-guide.md) collects every trap FMUpgradeTool sets without telling you, found the hard way on real files. Useful even if you never use Patchwork. A taste:
195
+
196
+ - without `--generateGUIDs`, theme and field patches are skipped;
197
+ - a layout's UUID is **not** the first `<UUID>` inside it;
198
+ - replacing one layout object is skipped, replacing the whole layout works;
199
+ - deleting a script step needs a `ScriptReference` inside each `ScriptStepReference`.
200
+
201
+ Found another one? [Add it](https://github.com/Abdeltoto/patchwork-filemaker-mcp/issues/new?template=field-guide.yml).
202
+
203
+ ## FAQ
204
+
205
+ <details>
206
+ <summary><b>Does it work on hosted files?</b></summary>
207
+ <br>
208
+ It works on closed <code>.fmp12</code> files on disk. Take a copy of the hosted file, edit it, test it, then deploy it as you usually do, or move the data with the Claris Data Migration Tool.
209
+ </details>
210
+
211
+ <details>
212
+ <summary><b>Can the agent change records?</b></summary>
213
+ <br>
214
+ No, FMUpgradeTool changes structure only. For records, use the Data API or OData. The <code>fm-data</code> skill explains how.
215
+ </details>
216
+
217
+ <details>
218
+ <summary><b>Which FileMaker versions?</b></summary>
219
+ <br>
220
+ Developed and verified with FileMaker 2026 (26.0.3). Earlier versions that ship FMUpgradeTool and <code>FMDeveloperTool --saveAsXML</code> should work but are untested. A report either way is very welcome.
221
+ </details>
222
+
223
+ <details>
224
+ <summary><b>macOS and Linux?</b></summary>
225
+ <br>
226
+ Wherever the Claris tools run. CI runs on Windows, macOS and Linux.
227
+ </details>
228
+
229
+ <details>
230
+ <summary><b>Is my password safe with an agent?</b></summary>
231
+ <br>
232
+ Yes. It is read from the <code>PATCHWORK_PASSWORD</code> environment variable, passed to the Claris tools, and printed as <code>***</code>. It is never written in a configuration file or shown to the agent.
233
+ </details>
234
+
235
+ ## Contributing
236
+
237
+ Found a new FMUpgradeTool trap, wrote a recipe, or want a new operation? Everything is welcome: start with [CONTRIBUTING.md](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/CONTRIBUTING.md). And if Patchwork gives you your Friday afternoon back, a ⭐ helps other FileMaker developers find it.
238
+
239
+ <a href="https://star-history.com/#Abdeltoto/patchwork-filemaker-mcp&Date">
240
+ <img src="https://api.star-history.com/svg?repos=Abdeltoto/patchwork-filemaker-mcp&type=Date" alt="Star history" width="600">
241
+ </a>
242
+
243
+ ## License
244
+
245
+ [MIT](https://github.com/Abdeltoto/patchwork-filemaker-mcp/blob/main/LICENSE). Patchwork is a community project, not affiliated with or endorsed by Claris International Inc. FileMaker and Claris are trademarks of Claris International Inc. Other product names and logos belong to their owners.
@@ -0,0 +1,28 @@
1
+ filemaker_patchwork-0.1.0.dist-info/licenses/LICENSE,sha256=tYpPJpuSYncF1HiTGOgl5M215KDfX2b7WhfpnGN0UPM,1093
2
+ patchwork/__init__.py,sha256=-TW4CHTQrh5b5aod0Jf_cGmaNOoIX2XLZFA0t9tn0sc,211
3
+ patchwork/__main__.py,sha256=hQWhtFhhRDv2VHKgv2GPoUboHChvzL9PPVZ3bJakvKs,61
4
+ patchwork/build.py,sha256=lFzj4HlhXp_A8TYtHdPStn_M2TG8lBR2gBrEzGMjU58,10099
5
+ patchwork/cli.py,sha256=VcObp7ZE1SeraAZziS39v_-kqleDXqSv9giMj8hsb3w,16604
6
+ patchwork/export.py,sha256=9evQDjFlVEEM0piJ3SBfz2WQcRPs9XIEx-L_7bUFyoU,6765
7
+ patchwork/mcp.py,sha256=xTvyqvfvomtzdZcdgZ6knrMIel82NC2iYttYMig-u6A,12684
8
+ patchwork/objects.py,sha256=azG8ksbR-BsL1qJ2acmYoXYW4EXmBAmI1xHfnpOhCRM,15326
9
+ patchwork/ops.py,sha256=twTiEJ7YnrziW1iJztM4bcGfZdUbPtKdcY0q0rS3t7k,12332
10
+ patchwork/place.py,sha256=HAnW1-J-Gb27i3L5WMFTWYiH25q4YosnbfGrJn2pRU4,9442
11
+ patchwork/project.py,sha256=iAqMhSnlHjLe1vVfsRMSmpZ0DDfm4bGzUybM5BWgwLo,3756
12
+ patchwork/rebrand.py,sha256=yPNrsAsQlCqvGnDLnJ71svSq-NJQ-23IeMzOJ1Ry6V4,5821
13
+ patchwork/script.py,sha256=CWYiLMbuYCWJYPSKPm0Q-NnKUcf9OLTTeAHbHtxl13M,20795
14
+ patchwork/tools.py,sha256=R5K2viv9Y1Ygw8gHkQqSrUvNScQFoq90QDX4YyQa2Tg,7293
15
+ patchwork/uses.py,sha256=A8d67oFX7o4X11bpW_ZOsdjhyKoagKeD3FEjVz5Uds8,6064
16
+ patchwork/workspace.py,sha256=wUoE0W2WVJLP_CBg6VcTe1D3Vx9fwPrlPjrBZpHfSbY,6827
17
+ patchwork/skills/fm-data/SKILL.md,sha256=rIZd1csH4AsKBhA-mSnyQeUjqlyQKNvMumclZJJYOyM,825
18
+ patchwork/skills/fm-explore/SKILL.md,sha256=oGRrc7_ifVOTTOong1o5MW5_6HGA9jl_TJpVYFQcghs,1080
19
+ patchwork/skills/fm-standards/SKILL.md,sha256=aHwfSlc_jOjeTPI64dWXM6aHWMzPY4OhLpPIv0Da58o,931
20
+ patchwork/skills/patchwork/SKILL.md,sha256=nzmJU0IS7Hz_HbSsL_N337xDxRNA9iUARehIni7FIyw,4767
21
+ patchwork/templates/AGENTS.md,sha256=ry3_Rf3zywEWj4FEv_UFTlS3uW95nIaPNdmZiZ6aGEI,2216
22
+ patchwork/templates/gitignore,sha256=_p3SXFZkpN-dtsh1FvMp5bUDHRiD9oAfIh8uOdbFNEc,132
23
+ patchwork/templates/naming.md,sha256=GkyqgjTvBPHvbftMgW8VJ9ZwFfprlj19nPrRe1IIZZE,344
24
+ filemaker_patchwork-0.1.0.dist-info/METADATA,sha256=KEcQZFbKyifxgkXaqiyzBzBI0Zf29su9iQRKsDsjr28,17220
25
+ filemaker_patchwork-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
26
+ filemaker_patchwork-0.1.0.dist-info/entry_points.txt,sha256=Ad6JebZC0Te3zIxYcJcXWZl9iUA2YnuLBTgBe43Pgog,90
27
+ filemaker_patchwork-0.1.0.dist-info/top_level.txt,sha256=aLF4R1vFf2HfqmKmZHlBkLny_-8Ye5nT-Pi8pKL2vE8,10
28
+ filemaker_patchwork-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ filemaker-patchwork = patchwork.cli:main
3
+ patchwork = patchwork.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdeltoto and Patchwork contributors
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.
@@ -0,0 +1 @@
1
+ patchwork
patchwork/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Patchwork: patch FileMaker files from code, with Claris FMUpgradeTool."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+
6
+ class PatchworkError(Exception):
7
+ """An error the user can fix. The CLI prints it without a traceback."""
patchwork/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from patchwork.cli import main
4
+
5
+ sys.exit(main())
patchwork/build.py ADDED
@@ -0,0 +1,249 @@
1
+ """Build FMUpgradeTool patches (root element FMUpgradeToolPatch) from an Export.
2
+
3
+ Rules learned on real files, all enforced here:
4
+ - Replace a whole layout or a whole theme. A lone LayoutObject replace, a ReplaceAction
5
+ around a LayoutCatalog and an AddAction of a new theme are skipped by FMUpgradeTool.
6
+ - An added field needs membercount="1", a <TagList> after the catalog UUID, and id = max + 1.
7
+ - A FieldReference in a layout may carry repetition="1"; it must be kept.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import io
14
+ import re
15
+ import uuid
16
+ from pathlib import Path
17
+
18
+ from patchwork import PatchworkError
19
+ from patchwork.export import Export, escape
20
+
21
+ PATCH_VERSION = "2.3.0.0"
22
+ CALC_MAX_CHARS = 28000
23
+ RGBA = re.compile(r"rgba\(([\d.]+)%,([\d.]+)%,([\d.]+)%,([\d.]+)\)")
24
+ FIELD_TYPES = {
25
+ "Text": "Text",
26
+ "Number": "Number",
27
+ "Date": "Date",
28
+ "Time": "Time",
29
+ "Timestamp": "TimeStamp",
30
+ "Container": "Binary",
31
+ }
32
+
33
+
34
+ def recolor(css: str, colors: dict[str, str]) -> str:
35
+ """Swap hex colors inside FileMaker rgba(r%,g%,b%,a) values. Alpha is kept."""
36
+ table = {normalize_hex(k): normalize_hex(v) for k, v in colors.items()}
37
+ if not table:
38
+ return css
39
+
40
+ def repl(match: re.Match) -> str:
41
+ rgb = "".join(f"{round(float(match.group(i)) * 2.55):02X}" for i in (1, 2, 3))
42
+ new = table.get(rgb)
43
+ if not new:
44
+ return match.group(0)
45
+ parts = [int(new[i:i + 2], 16) / 255 * 100 for i in (0, 2, 4)]
46
+ values = ",".join(f"{p:.4f}".rstrip("0").rstrip(".") + "%" for p in parts)
47
+ return f"rgba({values},{match.group(4)})"
48
+
49
+ return RGBA.sub(repl, css)
50
+
51
+
52
+ def palette(css: str) -> list[tuple[str, int]]:
53
+ """Hex colors used in a block of FileMaker CSS, most frequent first."""
54
+ found = ["".join(f"{round(float(m.group(i)) * 2.55):02X}" for i in (1, 2, 3)) for m in RGBA.finditer(css)]
55
+ counts: dict[str, int] = {}
56
+ for color in found:
57
+ counts[color] = counts.get(color, 0) + 1
58
+ return sorted(counts.items(), key=lambda item: (-item[1], item[0]))
59
+
60
+
61
+ def palette(css: str) -> list[tuple[str, int]]:
62
+ """Hex colors used in a block of FileMaker CSS, most frequent first."""
63
+ found = ["".join(f"{round(float(m.group(i)) * 2.55):02X}" for i in (1, 2, 3)) for m in RGBA.finditer(css)]
64
+ counts: dict[str, int] = {}
65
+ for color in found:
66
+ counts[color] = counts.get(color, 0) + 1
67
+ return sorted(counts.items(), key=lambda item: (-item[1], item[0]))
68
+
69
+
70
+ def normalize_hex(value: str) -> str:
71
+ value = value.strip().lstrip("#").upper()
72
+ if not re.fullmatch(r"[0-9A-F]{6}", value):
73
+ raise PatchworkError(f"Color must be a 6-digit hex value like 2F5D50: {value}")
74
+ return value
75
+
76
+
77
+ def remap_fonts(css: str, fonts: dict[str, str]) -> str:
78
+ """Fonts are written -fm-font-family(WindowsName,MacPostScriptName)."""
79
+ for old, new in fonts.items():
80
+ css = css.replace(f"-fm-font-family({old})", f"-fm-font-family({new})")
81
+ return css
82
+
83
+
84
+ def wrap(*actions: str, version: str = PATCH_VERSION) -> str:
85
+ """The patch version must equal the FMSaveAsXML version of the export it was built from."""
86
+ body = "".join(a for a in actions if a)
87
+ return (
88
+ '<?xml version="1.0" encoding="UTF-8"?>\n'
89
+ f'<FMUpgradeToolPatch version="{version}">\n\t<Structure>\n'
90
+ f"{body}\t</Structure>\n</FMUpgradeToolPatch>\n"
91
+ )
92
+
93
+
94
+ def replace_action(blocks: list[tuple[str, str, str]]) -> str:
95
+ """blocks: (type, UUID, XML) for each Replace."""
96
+ if not blocks:
97
+ return ""
98
+ inner = "".join(f'\t\t\t<Replace type="{kind}" UUID="{uid}">\n{xml}\n\t\t\t</Replace>\n' for kind, uid, xml in blocks)
99
+ return f"\t\t<ReplaceAction>\n{inner}\t\t</ReplaceAction>\n"
100
+
101
+
102
+ def field_xml(
103
+ export: Export,
104
+ table: str,
105
+ name: str,
106
+ kind: str = "Text",
107
+ comment: str = "",
108
+ is_global: bool = False,
109
+ calc: str | None = None,
110
+ context: str | None = None,
111
+ ) -> str:
112
+ if kind not in FIELD_TYPES:
113
+ raise PatchworkError(f"Unknown field type: {kind}. Choose from {', '.join(FIELD_TYPES)}.")
114
+ if export.has_field(table, name):
115
+ raise PatchworkError(f"{table}::{name} already exists.")
116
+ field_id = export.next_field_id(table)
117
+ head = (
118
+ f'<Field id="{field_id}" name="{escape(name)}" fieldtype="{"Calculated" if calc else "Normal"}" '
119
+ f'datatype="{FIELD_TYPES[kind]}" comment="{escape(comment)}">\n'
120
+ f"\t<UUID>{new_uuid()}</UUID>\n"
121
+ )
122
+ language = '\t\t<LanguageReference name="English" id="21"></LanguageReference>\n'
123
+ flag = "True" if is_global else "False"
124
+ if calc:
125
+ if len(calc) > CALC_MAX_CHARS:
126
+ raise PatchworkError(
127
+ f"Calculation too long ({len(calc)} characters, max {CALC_MAX_CHARS}). "
128
+ "For an image, pass a smaller width."
129
+ )
130
+ occurrence = export.table_occurrence(context or table)
131
+ body = (
132
+ '\t<AutoEnter alwaysEvaluate="False"></AutoEnter>\n'
133
+ f'\t<Storage storeCalculationResults="True" global="{flag}" maxRepetitions="1">\n{language}\t</Storage>\n'
134
+ f"\t<Calculation>\n\t\t{occurrence}\n\t\t<Text><![CDATA[{calc}]]></Text>\n\t</Calculation>\n"
135
+ )
136
+ else:
137
+ storage = (
138
+ f'\t<Storage global="{flag}" maxRepetitions="1"></Storage>\n'
139
+ if kind == "Container"
140
+ else f'\t<Storage autoIndex="True" index="None" global="{flag}" maxRepetitions="1">\n{language}\t</Storage>\n'
141
+ )
142
+ body = (
143
+ '\t<AutoEnter type="" prohibitModification="False"></AutoEnter>\n'
144
+ '\t<Validation alwaysValidate="False" type="OnlyDuringDataEntry" allowOverride="True" '
145
+ 'notEmpty="False" unique="False" existing="False"></Validation>\n'
146
+ f"{storage}"
147
+ )
148
+ return head + body + '\t<TagList></TagList>\n\t<Annotation></Annotation>\n\t<DisplayNames enable="False"></DisplayNames>\n</Field>\n'
149
+
150
+
151
+ def add_field_action(export: Export, table: str, field: str) -> str:
152
+ catalog_uuid, base_ref, _ = export.field_catalog(table)
153
+ return (
154
+ "\t\t<AddAction>\n"
155
+ '\t\t\t<FieldsForTables membercount="1">\n'
156
+ f"\t\t\t\t<FieldCatalog>\n\t\t\t\t\t<UUID>{catalog_uuid}</UUID>\n"
157
+ "\t\t\t\t\t<TagList></TagList>\n"
158
+ f"\t\t\t\t\t{base_ref}\n"
159
+ f'\t\t\t\t\t<ObjectList membercount="1">\n{field}\t\t\t\t\t</ObjectList>\n'
160
+ "\t\t\t\t</FieldCatalog>\n\t\t\t</FieldsForTables>\n"
161
+ "\t\t</AddAction>\n"
162
+ )
163
+
164
+
165
+ def add_field_patch(export: Export, table: str, field: str) -> str:
166
+ return wrap(add_field_action(export, table, field), version=export.version)
167
+
168
+
169
+ def image_calc(png: Path, width: int | None = None) -> str:
170
+ """A calculation that turns an embedded PNG into container data."""
171
+ if not png.is_file():
172
+ raise PatchworkError(f"Image not found: {png}")
173
+ data = png.read_bytes()
174
+ if width:
175
+ try:
176
+ from PIL import Image
177
+ except ImportError as exc:
178
+ raise PatchworkError("Resizing an image needs Pillow: pip install \"filemaker-patchwork[image]\"") from exc
179
+ img = Image.open(png).convert("RGBA")
180
+ img = img.resize((width, round(img.height * width / img.width)), Image.LANCZOS)
181
+ buf = io.BytesIO()
182
+ img.save(buf, "PNG", optimize=True)
183
+ data = buf.getvalue()
184
+ encoded = base64.b64encode(data).decode("ascii")
185
+ return f'Base64Decode ( "{encoded}" ; "{png.stem}.png" )'
186
+
187
+
188
+ def theme_replace(export: Export, label: str, rename: str | None, colors: dict, fonts: dict) -> str:
189
+ theme = export.theme(label)
190
+ xml = recolor(remap_fonts(theme.xml, fonts), colors)
191
+ if rename:
192
+ xml = re.sub(r'Display="[^"]*"', f'Display="{escape(rename)}"', xml, count=1)
193
+ return replace_action([("Theme", theme.uuid, xml)])
194
+
195
+
196
+ def theme_patch(export: Export, label: str, rename: str | None, colors: dict, fonts: dict) -> str:
197
+ return wrap(theme_replace(export, label, rename, colors, fonts), version=export.version)
198
+
199
+
200
+ def layouts_patch(
201
+ export: Export,
202
+ only: list[str] | None = None,
203
+ colors: dict | None = None,
204
+ fonts: dict | None = None,
205
+ remap: tuple[str, str] | None = None,
206
+ ) -> tuple[str, list[str]]:
207
+ """Rewrite only the layouts that change. Returns (patch, names of changed layouts)."""
208
+ colors, fonts = colors or {}, fonts or {}
209
+ pattern = target = None
210
+ if remap:
211
+ (src_table, src_name), (dst_table, dst_name) = split_field(remap[0]), split_field(remap[1])
212
+ if src_table != dst_table:
213
+ raise PatchworkError("A field can only be swapped for another field of the same table.")
214
+ _, src_uuid = export.field(src_table, src_name)
215
+ dst_id, dst_uuid = export.field(dst_table, dst_name)
216
+ pattern = re.compile(rf'<FieldReference id="\d+" name="[^"]*"( repetition="\d+")? UUID="{src_uuid}">')
217
+ target = rf'<FieldReference id="{dst_id}" name="{escape(dst_name)}"\1 UUID="{dst_uuid}">'
218
+ if only:
219
+ known = {layout.name for layout in export.layouts()}
220
+ missing = [name for name in only if name not in known]
221
+ if missing:
222
+ raise PatchworkError(f"Layout not found: {', '.join(missing)}")
223
+ blocks, names = [], []
224
+ for layout in export.layouts():
225
+ if only and layout.name not in only:
226
+ continue
227
+ xml = recolor(remap_fonts(layout.xml, fonts), colors)
228
+ if pattern:
229
+ xml = pattern.sub(target, xml)
230
+ if xml != layout.xml:
231
+ blocks.append(("Layout", layout.uuid, xml))
232
+ names.append(layout.name)
233
+ return wrap(replace_action(blocks), version=export.version), names
234
+
235
+
236
+ def extract_layout_patch(export: Export, name: str) -> str:
237
+ layout = export.layout(name)
238
+ return wrap(replace_action([("Layout", layout.uuid, layout.xml)]), version=export.version)
239
+
240
+
241
+ def split_field(qualified: str) -> tuple[str, str]:
242
+ if "::" not in qualified:
243
+ raise PatchworkError(f"Expected a field as Table::Field, got: {qualified}")
244
+ table, name = qualified.split("::", 1)
245
+ return table, name
246
+
247
+
248
+ def new_uuid() -> str:
249
+ return str(uuid.uuid4()).upper()