flowcvcli 0.6.0__tar.gz → 0.7.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.
Files changed (51) hide show
  1. {flowcvcli-0.6.0/flowcvcli.egg-info → flowcvcli-0.7.0}/PKG-INFO +66 -13
  2. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/README.md +65 -12
  3. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/docs/API.md +1 -1
  4. flowcvcli-0.7.0/docs/PULLPUSH.md +87 -0
  5. flowcvcli-0.7.0/flowcvcli/__init__.py +13 -0
  6. flowcvcli-0.7.0/flowcvcli/cli.py +811 -0
  7. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli/client.py +142 -31
  8. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli/config.py +56 -2
  9. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli/content.py +39 -21
  10. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli/customization.py +7 -5
  11. flowcvcli-0.7.0/flowcvcli/errors.py +29 -0
  12. flowcvcli-0.7.0/flowcvcli/jsonresume.py +350 -0
  13. flowcvcli-0.7.0/flowcvcli/markup.py +159 -0
  14. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli/personal.py +4 -2
  15. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli/photo.py +4 -3
  16. flowcvcli-0.7.0/flowcvcli/repo.py +346 -0
  17. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli/resume.py +61 -5
  18. {flowcvcli-0.6.0 → flowcvcli-0.7.0/flowcvcli.egg-info}/PKG-INFO +66 -13
  19. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli.egg-info/SOURCES.txt +22 -1
  20. flowcvcli-0.7.0/tests/test_batch.py +101 -0
  21. flowcvcli-0.7.0/tests/test_cli.py +72 -0
  22. flowcvcli-0.7.0/tests/test_client_auth.py +103 -0
  23. flowcvcli-0.7.0/tests/test_config.py +54 -0
  24. flowcvcli-0.7.0/tests/test_content.py +106 -0
  25. flowcvcli-0.7.0/tests/test_customize_cli.py +105 -0
  26. flowcvcli-0.7.0/tests/test_doctor.py +155 -0
  27. flowcvcli-0.7.0/tests/test_edit.py +113 -0
  28. flowcvcli-0.7.0/tests/test_errors.py +58 -0
  29. flowcvcli-0.7.0/tests/test_json_output.py +223 -0
  30. flowcvcli-0.7.0/tests/test_jsonresume.py +397 -0
  31. flowcvcli-0.7.0/tests/test_jsonresume_cli.py +99 -0
  32. flowcvcli-0.7.0/tests/test_markup.py +148 -0
  33. flowcvcli-0.7.0/tests/test_pullpush.py +359 -0
  34. flowcvcli-0.7.0/tests/test_ratelimit.py +89 -0
  35. flowcvcli-0.7.0/tests/test_resume.py +63 -0
  36. flowcvcli-0.7.0/tests/test_snapshots.py +157 -0
  37. flowcvcli-0.6.0/flowcvcli/__init__.py +0 -8
  38. flowcvcli-0.6.0/flowcvcli/cli.py +0 -405
  39. flowcvcli-0.6.0/flowcvcli/markup.py +0 -62
  40. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/.env.example +0 -0
  41. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/LICENSE +0 -0
  42. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/MANIFEST.in +0 -0
  43. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/docs/RENDERING.md +0 -0
  44. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli/__main__.py +0 -0
  45. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli/api.py +0 -0
  46. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli.egg-info/dependency_links.txt +0 -0
  47. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli.egg-info/entry_points.txt +0 -0
  48. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli.egg-info/requires.txt +0 -0
  49. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/flowcvcli.egg-info/top_level.txt +0 -0
  50. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/pyproject.toml +0 -0
  51. {flowcvcli-0.6.0 → flowcvcli-0.7.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowcvcli
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Control a FlowCV resume from the command line or Python — content, design, templates, photo, publish and PDF export — via FlowCV's private JSON API.
5
5
  Author: dannyota
6
6
  License-Expression: MIT
@@ -37,10 +37,14 @@ Dynamic: license-file
37
37
 
38
38
  Control a [FlowCV](https://flowcv.com) resume from the **command line** or from
39
39
  **Python** — content, header & links, **customization**, **templates**,
40
- **avatar**, reorder/hide, multi-resume management, **backup/restore**, publish, and **PDF export**.
40
+ **avatar**, reorder/hide, multi-resume management, **backup/restore**, publish,
41
+ **PDF export**, **resume-as-code** (`pull`/`push` a git-diffable Markdown tree),
42
+ **JSON Resume** import/export, `$EDITOR` editing, auto-snapshots before
43
+ destructive ops, and machine-readable **`--json`** output for every command.
41
44
  It drives FlowCV's private JSON API (the same calls the web app makes), so it
42
- works for any FlowCV resume with your own session. **Zero dependencies** (Python
43
- standard library only), so it's easy to drop into scripts and LLM agents.
45
+ works for any FlowCV resume with your own session. One tiny dependency
46
+ (`curl_cffi`, only for the login TLS fingerprint); everything else is the Python
47
+ standard library, so it drops easily into scripts and LLM agents.
44
48
 
45
49
  > Unofficial and not affiliated with FlowCV. It uses FlowCV's undocumented
46
50
  > internal API and may break if that changes; use it with your own account and at
@@ -103,7 +107,8 @@ flowcv add work --set title="Engineer" --set company="Acme" \
103
107
  --set start=01/2022 --set end=Present --text $'- Did a measurable thing.'
104
108
  flowcv add custom2 --section-name "Open Source" --icon code --text "…" # heading+icon at creation
105
109
  flowcv desc work <id> --file role.md # rich text; --field is optional (auto: profile=text, skill=infoHtml)
106
- flowcv date publication <id> --year 2018 # structured date (year-only by default)
110
+ flowcv edit work <id> # open the entry's rich text as markdown in $EDITOR, save on change
111
+ flowcv date publication <id> --year 2018 # structured date; merges (only passed parts change), --clear resets
107
112
  flowcv field work <id> employer --text "Acme Corp"
108
113
  flowcv rm work <id>
109
114
 
@@ -114,6 +119,7 @@ flowcv rename-section skill "Core Skills"
114
119
  flowcv section-icon skill head-side-brain
115
120
  flowcv rm-section custom1 --yes # delete a section + its entries
116
121
  flowcv reorder-sections profile work skill education # set one-column order (no args = print current)
122
+ flowcv reorder-sections work skill --layout two --side left # two-column: order one column at a time
117
123
 
118
124
  # header details & links (links are social entries: orcid, googlescholar, github…)
119
125
  flowcv pd jobTitle --text "Security Leader"
@@ -125,26 +131,47 @@ flowcv avatar set https://example.com/me.png # upload from URL or file
125
131
  flowcv avatar on | off | remove
126
132
 
127
133
  # styling (a delta into resume.customization) and templates
128
- flowcv customize font.fontFamily "Source Sans Pro"
134
+ flowcv customize # no value = print the current customization tree
135
+ flowcv customize font # filter to a subtree (leaves as dot.path = value)
136
+ flowcv customize font.fontFamily "Source Sans Pro" # with a value = set that dot-path
129
137
  flowcv customize colors.basic.single '"#0e374e"'
138
+ flowcv icons # list commonly-used section iconKeys
130
139
  flowcv templates # lists each as [free] / [PAID] (paid needs a subscription)
131
140
  flowcv apply-template <templateId> # warns first if the template is paid
132
141
 
133
142
  # render & share
134
- flowcv download -o resume.pdf # the rendered PDF
143
+ flowcv download -o resume.pdf # the rendered PDF (--pages N caps rendered pages; default 10)
135
144
  flowcv download --token <webToken> -o out.pdf # any PUBLIC resume by its share token (no auth)
136
145
  flowcv share | publish | unpublish
137
146
 
138
147
  # backup / restore
139
148
  flowcv export -o backup.json # full resume snapshot (JSON) — keep one before big edits
140
149
  flowcv import backup.json # restore the snapshot into a NEW resume (non-destructive)
150
+ flowcv export --format jsonresume -o me.json # export to the jsonresume.org schema
151
+ flowcv import --format jsonresume me.json # build a NEW resume from a JSON Resume doc
152
+ flowcv backups # list auto-snapshots (rm-section / delete-resume snapshot first; --no-backup opts out)
141
153
 
142
154
  flowcv login # refresh the cached session
155
+ flowcv doctor # diagnose auth/session/setup (--offline skips the live probe)
143
156
  flowcv md2html --file role.md # preview HTML (offline)
144
157
  ```
145
158
 
146
- Any command takes `--resume-id <id>` to target a specific resume. (From source,
147
- replace `flowcv` with `python3 flowcv.py`.)
159
+ Any command takes `--resume-id <id>` to target a specific resume, and `--json`
160
+ (before or after the subcommand) to emit exactly one machine-readable JSON
161
+ document instead of human text — errors become `{"error", "type"}` on stdout
162
+ with exit 1. (From source, replace `flowcv` with `python3 flowcv.py`.)
163
+
164
+ ### Resume as code
165
+
166
+ ```bash
167
+ flowcv pull [dir] # materialize the resume as a Markdown tree (default ./resume)
168
+ # edit the files: personal.md, NN-<section>/_section.md, NN-<id8>.md (frontmatter + body)
169
+ flowcv push [dir] --dry-run # review the diff (last-writer-wins; no 3-way merge)
170
+ flowcv push [dir] # apply ONLY changes (edits, adds, deletes, reorder, renames)
171
+ ```
172
+
173
+ `pull` then an immediate `push` is a no-op. Full layout, frontmatter rules and
174
+ round-trip caveats: [`docs/PULLPUSH.md`](docs/PULLPUSH.md).
148
175
 
149
176
  ## Library (for scripts & LLM agents)
150
177
 
@@ -167,12 +194,29 @@ fc.rename_section("skill", "Core Skills"); fc.delete_section("custom1")
167
194
  fc.hide_entry("work", "id", hidden=True)
168
195
  new_id = fc.create_resume("Second Resume") # or fc.duplicate_resume()
169
196
  fc.rename_resume("New Title"); fc.delete_resume() # delete is permanent
170
- fc.set_date("publication", "id", year=2018) # structured date (year-only)
197
+ fc.set_date("publication", "id", year=2018) # structured date; merges (only passed parts change)
171
198
 
172
199
  # backup / restore
173
200
  import json
174
201
  json.dump(fc.export_resume(), open("backup.json", "w")) # full snapshot
175
202
  new_id = fc.import_resume(json.load(open("backup.json"))) # restore into a NEW resume
203
+
204
+ # JSON Resume interop (jsonresume.org schema)
205
+ from flowcvcli import to_jsonresume, from_jsonresume
206
+ jr = to_jsonresume(fc.export_resume()) # FlowCV -> JSON Resume dict
207
+ fc.import_resume(from_jsonresume(jr, base=fc.get_resume())) # JSON Resume -> a NEW resume
208
+
209
+ # many edits? batch caches the read (1 GET instead of N against the rate limit)
210
+ with fc.batch():
211
+ fc.set_field("work", "id", "employer", "Acme Corp")
212
+ fc.set_date("publication", "id", year=2018)
213
+
214
+ # errors are normal exceptions (they never SystemExit your process):
215
+ from flowcvcli import FlowCVError, AuthError, RateLimitError, NotFoundError
216
+ try:
217
+ fc.find_entry(fc.get_resume(), "work", "bad-id")
218
+ except NotFoundError as e:
219
+ print(e) # all subclass FlowCVError
176
220
  ```
177
221
 
178
222
  ### Build → render → check → improve
@@ -193,6 +237,10 @@ feedback loop for building a resume from raw info.
193
237
  | `**bold**` / `***bold-italic***` inline | `<strong>` / `<strong><em>…</em></strong>` |
194
238
  | `[text](url)` inline | link |
195
239
 
240
+ Bold markers inside an already-bold line or `**span**` are redundant and
241
+ collapse to plain text — this keeps markdown ⇄ HTML round-trips stable
242
+ (`flowcv edit`, `pull`/`push`).
243
+
196
244
  ## How it works
197
245
 
198
246
  - **Read-modify-write**: edits fetch the resume, change one part, and send it
@@ -210,16 +258,21 @@ feedback loop for building a resume from raw info.
210
258
  ```
211
259
  flowcvcli/ # the package (import flowcvcli)
212
260
  config.py # resolve resume id + auth from .env / env vars
213
- client.py # HTTP, login, cookie-jar session, retry, get_resume
214
- markup.py # markdown <-> FlowCV rich-text HTML
261
+ client.py # HTTP, login, cookie-jar session, retry, batch cache
262
+ errors.py # FlowCVError hierarchy (Auth/RateLimit/NotFound/Api)
263
+ markup.py # markdown <-> FlowCV rich-text HTML (both directions)
215
264
  content.py # sections & entries (add/edit/reorder/hide/sections)
216
265
  personal.py # header details & links
217
266
  customization.py # styling deltas & templates
267
+ jsonresume.py # JSON Resume (jsonresume.org) import/export
268
+ repo.py # resume-as-code: pull/push Markdown tree
218
269
  photo.py # avatar upload / toggle
219
- resume.py # list, create/duplicate/rename/delete, download, publish
270
+ resume.py # list, create/duplicate/rename/delete, download, publish, snapshots
220
271
  api.py # FlowCV = Client + all mixins
221
272
  cli.py / __main__.py # the `flowcv` command
273
+ tests/ # offline unit tests: python3 -m unittest discover -s tests -t .
222
274
  docs/API.md # reverse-engineered API reference
275
+ docs/PULLPUSH.md # resume-as-code design (layout, frontmatter, diff rules)
223
276
  docs/RENDERING.md # how the editor renders the preview & debounces saves
224
277
  flowcv.py # source-tree entry point (python3 flowcv.py …)
225
278
  ```
@@ -6,10 +6,14 @@
6
6
 
7
7
  Control a [FlowCV](https://flowcv.com) resume from the **command line** or from
8
8
  **Python** — content, header & links, **customization**, **templates**,
9
- **avatar**, reorder/hide, multi-resume management, **backup/restore**, publish, and **PDF export**.
9
+ **avatar**, reorder/hide, multi-resume management, **backup/restore**, publish,
10
+ **PDF export**, **resume-as-code** (`pull`/`push` a git-diffable Markdown tree),
11
+ **JSON Resume** import/export, `$EDITOR` editing, auto-snapshots before
12
+ destructive ops, and machine-readable **`--json`** output for every command.
10
13
  It drives FlowCV's private JSON API (the same calls the web app makes), so it
11
- works for any FlowCV resume with your own session. **Zero dependencies** (Python
12
- standard library only), so it's easy to drop into scripts and LLM agents.
14
+ works for any FlowCV resume with your own session. One tiny dependency
15
+ (`curl_cffi`, only for the login TLS fingerprint); everything else is the Python
16
+ standard library, so it drops easily into scripts and LLM agents.
13
17
 
14
18
  > Unofficial and not affiliated with FlowCV. It uses FlowCV's undocumented
15
19
  > internal API and may break if that changes; use it with your own account and at
@@ -72,7 +76,8 @@ flowcv add work --set title="Engineer" --set company="Acme" \
72
76
  --set start=01/2022 --set end=Present --text $'- Did a measurable thing.'
73
77
  flowcv add custom2 --section-name "Open Source" --icon code --text "…" # heading+icon at creation
74
78
  flowcv desc work <id> --file role.md # rich text; --field is optional (auto: profile=text, skill=infoHtml)
75
- flowcv date publication <id> --year 2018 # structured date (year-only by default)
79
+ flowcv edit work <id> # open the entry's rich text as markdown in $EDITOR, save on change
80
+ flowcv date publication <id> --year 2018 # structured date; merges (only passed parts change), --clear resets
76
81
  flowcv field work <id> employer --text "Acme Corp"
77
82
  flowcv rm work <id>
78
83
 
@@ -83,6 +88,7 @@ flowcv rename-section skill "Core Skills"
83
88
  flowcv section-icon skill head-side-brain
84
89
  flowcv rm-section custom1 --yes # delete a section + its entries
85
90
  flowcv reorder-sections profile work skill education # set one-column order (no args = print current)
91
+ flowcv reorder-sections work skill --layout two --side left # two-column: order one column at a time
86
92
 
87
93
  # header details & links (links are social entries: orcid, googlescholar, github…)
88
94
  flowcv pd jobTitle --text "Security Leader"
@@ -94,26 +100,47 @@ flowcv avatar set https://example.com/me.png # upload from URL or file
94
100
  flowcv avatar on | off | remove
95
101
 
96
102
  # styling (a delta into resume.customization) and templates
97
- flowcv customize font.fontFamily "Source Sans Pro"
103
+ flowcv customize # no value = print the current customization tree
104
+ flowcv customize font # filter to a subtree (leaves as dot.path = value)
105
+ flowcv customize font.fontFamily "Source Sans Pro" # with a value = set that dot-path
98
106
  flowcv customize colors.basic.single '"#0e374e"'
107
+ flowcv icons # list commonly-used section iconKeys
99
108
  flowcv templates # lists each as [free] / [PAID] (paid needs a subscription)
100
109
  flowcv apply-template <templateId> # warns first if the template is paid
101
110
 
102
111
  # render & share
103
- flowcv download -o resume.pdf # the rendered PDF
112
+ flowcv download -o resume.pdf # the rendered PDF (--pages N caps rendered pages; default 10)
104
113
  flowcv download --token <webToken> -o out.pdf # any PUBLIC resume by its share token (no auth)
105
114
  flowcv share | publish | unpublish
106
115
 
107
116
  # backup / restore
108
117
  flowcv export -o backup.json # full resume snapshot (JSON) — keep one before big edits
109
118
  flowcv import backup.json # restore the snapshot into a NEW resume (non-destructive)
119
+ flowcv export --format jsonresume -o me.json # export to the jsonresume.org schema
120
+ flowcv import --format jsonresume me.json # build a NEW resume from a JSON Resume doc
121
+ flowcv backups # list auto-snapshots (rm-section / delete-resume snapshot first; --no-backup opts out)
110
122
 
111
123
  flowcv login # refresh the cached session
124
+ flowcv doctor # diagnose auth/session/setup (--offline skips the live probe)
112
125
  flowcv md2html --file role.md # preview HTML (offline)
113
126
  ```
114
127
 
115
- Any command takes `--resume-id <id>` to target a specific resume. (From source,
116
- replace `flowcv` with `python3 flowcv.py`.)
128
+ Any command takes `--resume-id <id>` to target a specific resume, and `--json`
129
+ (before or after the subcommand) to emit exactly one machine-readable JSON
130
+ document instead of human text — errors become `{"error", "type"}` on stdout
131
+ with exit 1. (From source, replace `flowcv` with `python3 flowcv.py`.)
132
+
133
+ ### Resume as code
134
+
135
+ ```bash
136
+ flowcv pull [dir] # materialize the resume as a Markdown tree (default ./resume)
137
+ # edit the files: personal.md, NN-<section>/_section.md, NN-<id8>.md (frontmatter + body)
138
+ flowcv push [dir] --dry-run # review the diff (last-writer-wins; no 3-way merge)
139
+ flowcv push [dir] # apply ONLY changes (edits, adds, deletes, reorder, renames)
140
+ ```
141
+
142
+ `pull` then an immediate `push` is a no-op. Full layout, frontmatter rules and
143
+ round-trip caveats: [`docs/PULLPUSH.md`](docs/PULLPUSH.md).
117
144
 
118
145
  ## Library (for scripts & LLM agents)
119
146
 
@@ -136,12 +163,29 @@ fc.rename_section("skill", "Core Skills"); fc.delete_section("custom1")
136
163
  fc.hide_entry("work", "id", hidden=True)
137
164
  new_id = fc.create_resume("Second Resume") # or fc.duplicate_resume()
138
165
  fc.rename_resume("New Title"); fc.delete_resume() # delete is permanent
139
- fc.set_date("publication", "id", year=2018) # structured date (year-only)
166
+ fc.set_date("publication", "id", year=2018) # structured date; merges (only passed parts change)
140
167
 
141
168
  # backup / restore
142
169
  import json
143
170
  json.dump(fc.export_resume(), open("backup.json", "w")) # full snapshot
144
171
  new_id = fc.import_resume(json.load(open("backup.json"))) # restore into a NEW resume
172
+
173
+ # JSON Resume interop (jsonresume.org schema)
174
+ from flowcvcli import to_jsonresume, from_jsonresume
175
+ jr = to_jsonresume(fc.export_resume()) # FlowCV -> JSON Resume dict
176
+ fc.import_resume(from_jsonresume(jr, base=fc.get_resume())) # JSON Resume -> a NEW resume
177
+
178
+ # many edits? batch caches the read (1 GET instead of N against the rate limit)
179
+ with fc.batch():
180
+ fc.set_field("work", "id", "employer", "Acme Corp")
181
+ fc.set_date("publication", "id", year=2018)
182
+
183
+ # errors are normal exceptions (they never SystemExit your process):
184
+ from flowcvcli import FlowCVError, AuthError, RateLimitError, NotFoundError
185
+ try:
186
+ fc.find_entry(fc.get_resume(), "work", "bad-id")
187
+ except NotFoundError as e:
188
+ print(e) # all subclass FlowCVError
145
189
  ```
146
190
 
147
191
  ### Build → render → check → improve
@@ -162,6 +206,10 @@ feedback loop for building a resume from raw info.
162
206
  | `**bold**` / `***bold-italic***` inline | `<strong>` / `<strong><em>…</em></strong>` |
163
207
  | `[text](url)` inline | link |
164
208
 
209
+ Bold markers inside an already-bold line or `**span**` are redundant and
210
+ collapse to plain text — this keeps markdown ⇄ HTML round-trips stable
211
+ (`flowcv edit`, `pull`/`push`).
212
+
165
213
  ## How it works
166
214
 
167
215
  - **Read-modify-write**: edits fetch the resume, change one part, and send it
@@ -179,16 +227,21 @@ feedback loop for building a resume from raw info.
179
227
  ```
180
228
  flowcvcli/ # the package (import flowcvcli)
181
229
  config.py # resolve resume id + auth from .env / env vars
182
- client.py # HTTP, login, cookie-jar session, retry, get_resume
183
- markup.py # markdown <-> FlowCV rich-text HTML
230
+ client.py # HTTP, login, cookie-jar session, retry, batch cache
231
+ errors.py # FlowCVError hierarchy (Auth/RateLimit/NotFound/Api)
232
+ markup.py # markdown <-> FlowCV rich-text HTML (both directions)
184
233
  content.py # sections & entries (add/edit/reorder/hide/sections)
185
234
  personal.py # header details & links
186
235
  customization.py # styling deltas & templates
236
+ jsonresume.py # JSON Resume (jsonresume.org) import/export
237
+ repo.py # resume-as-code: pull/push Markdown tree
187
238
  photo.py # avatar upload / toggle
188
- resume.py # list, create/duplicate/rename/delete, download, publish
239
+ resume.py # list, create/duplicate/rename/delete, download, publish, snapshots
189
240
  api.py # FlowCV = Client + all mixins
190
241
  cli.py / __main__.py # the `flowcv` command
242
+ tests/ # offline unit tests: python3 -m unittest discover -s tests -t .
191
243
  docs/API.md # reverse-engineered API reference
244
+ docs/PULLPUSH.md # resume-as-code design (layout, frontmatter, diff rules)
192
245
  docs/RENDERING.md # how the editor renders the preview & debounces saves
193
246
  flowcv.py # source-tree entry point (python3 flowcv.py …)
194
247
  ```
@@ -37,7 +37,7 @@ on the same cookie jar → the jar now holds the authenticated `flowcvsidapp`.
37
37
  |---|---|---|---|
38
38
  | GET | `/resumes/all` | — | `data.resumes[]` (id, title, webToken, webResumeLive, order, …) |
39
39
  | GET | `/resumes/{resumeId}` | — | `data.resume` (full resume object) |
40
- | POST | `/resumes/create` | `{clientResume: {…full resume object…}}` | create a resume. The body must be a **complete** resume object (every NOT-NULL column), so the reliable way is to **clone a full existing resume** (`GET /resumes/{id}`), reassign `id`+`uuid`, set `title`, empty `content` (or keep it for a duplicate), and **drop** `webToken`/`feedbackToken`/`createdAt`/`updatedAt` (server regenerates). A hand-built partial body fails with Postgres `23502` (not-null). Note: the one-resume free-plan cap is **not** enforced on this endpoint. |
40
+ | POST | `/resumes/create` | `{clientResume: {…full resume object…}}` | create a resume. The body must be a **complete** resume object (every NOT-NULL column), so the reliable way is to **clone a full existing resume** (`GET /resumes/{id}`), reassign `id`+`uuid`, set `title`, empty `content` (or keep it for a duplicate), and **drop** `webToken`/`feedbackToken`/`createdAt`/`updatedAt` (server regenerates). A hand-built partial body fails with Postgres `23502` (not-null). Note: the one-resume free-plan cap is **not** enforced on this endpoint. **The server IGNORES the client-supplied `id` and mints its own** — the created resume comes back in `data.resume`; read the real id from there (using the client id yields `400 reloadClient:true` on every follow-up). `webResumeLive` IS honored from the clone: cloning a live resume publishes the copy under a fresh `webToken` unless you set it `false`. |
41
41
  | POST | `/resumes/duplicate` | `{resumeId}` | native duplicate — but returned a generic error in testing; duplicating via `create` (clone, keep `content`) is what this tool does instead. |
42
42
  | PATCH | `/resumes/rename_resume` | `{resumeId, resumeTitle}` | rename a resume |
43
43
  | DELETE | `/resumes/delete_resume?resumeId={id}` | — | **permanently delete** a resume (irreversible) |
@@ -0,0 +1,87 @@
1
+ # Resume as code — `flowcv pull` / `flowcv push`
2
+
3
+ Materialize a FlowCV resume as an editable, git-diffable tree of Markdown files,
4
+ edit it locally, then push only the changes back. Last-writer-wins; there is no
5
+ 3-way merge — `push --dry-run` is the review tool.
6
+
7
+ ## File layout (`./resume/` by default)
8
+
9
+ ```
10
+ resume/
11
+ .flowcv.json # manifest: resumeId, pulledAt, {entryId: relpath}
12
+ personal.md # personalDetails as frontmatter, NO body
13
+ 00-profile/
14
+ _section.md # frontmatter: displayName, iconKey, sectionType
15
+ 00-<id8>.md # one entry: frontmatter (scalars) + markdown body
16
+ 01-work/
17
+ _section.md
18
+ 00-<id8>.md
19
+ 01-<id8>.md
20
+
21
+ ```
22
+
23
+ - Section dirs are `NN-<sectionId>`, `NN` = 2-digit index in the resume's
24
+ `content` order (display order). Entry files are `NN-<first 8 of id>.md`,
25
+ `NN` = the entry's index within the section (its order).
26
+ - **Filenames are cosmetic** — ordering + a human hint. The authoritative entry
27
+ id is the `id` in each file's frontmatter; the manifest maps every full id to
28
+ its file. `push` addresses entries by that id, never by the filename.
29
+
30
+ ## Frontmatter format (tiny, no PyYAML)
31
+
32
+ Plain `key: value` lines between `---` fences. A value is written **verbatim**
33
+ when it is a string that survives the round-trip; otherwise it is
34
+ `json.dumps`-encoded (numbers, bools, null, lists, dicts, and any string that
35
+ would otherwise parse as JSON, has leading/trailing space, or contains a
36
+ newline). Parsing is the exact inverse: `json.loads` the value; on failure it is
37
+ a plain string. So `startDateNew: 01/2022` and `endDateNew: Present` stay plain,
38
+ `isHidden: false` is a bool, `endDateNew: "2019"` is forced-quoted to stay a
39
+ string, and `date: {"year": "2018", …}` is JSON. `personal.md` puts the whole
40
+ `personalDetails` object (scalars, `social`, `detailsOrder`, `photo`, …) into
41
+ frontmatter this way.
42
+
43
+ ## Entry body
44
+
45
+ The body is `html_to_md` of the entry's rich-text field (`text` for profile,
46
+ `infoHtml` for skill, else `description`; see `content.rich_field`). All other
47
+ fields are frontmatter. On push the body is compared in **markdown** space
48
+ (`html_to_md(live)` vs the file), and only re-encoded with `md_to_html` when it
49
+ differs — this is what makes the round-trip stable despite `md_to_html` /
50
+ `html_to_md` not being byte-inverse on non-canonical server HTML.
51
+
52
+ ## `push` — diff one snapshot, apply only changes
53
+
54
+ One `get_resume()` under `batch()`; every diff is computed against that snapshot,
55
+ then applied with low-level writers (no re-reads):
56
+
57
+ | Change detected | API call |
58
+ |---|---|
59
+ | entry frontmatter field and/or body differs | `save_entry` (merged onto the live entry, `updatedAt` bumped) |
60
+ | entry file deleted (live id not referenced by any file) | `delete_entry` |
61
+ | entry file with **no** `id` in frontmatter | create (two `save_entry` calls); new id written back into the file + manifest |
62
+ | section entry order (file `NN` order) differs | `reorder_entries` |
63
+ | `_section.md` displayName / iconKey differs | `rename_section` / `set_section_icon` |
64
+ | `personal.md` differs | `save_personal` (merged onto live `personalDetails`) |
65
+
66
+ Semantics: **overlay, never destroy.** Frontmatter/personal keys you delete from
67
+ a file are left untouched on the server; `push` never deletes a section or the
68
+ resume, and never creates a section (folders whose sectionId isn't live are
69
+ skipped). A frontmatter `id` that matches no live entry is skipped with a note.
70
+ `--dry-run` computes and prints/`--json`-emits the actions without applying any.
71
+
72
+ ## Round-trip guarantee
73
+
74
+ `pull` then an immediate `push` yields **zero actions**. `pull` writes every live
75
+ field verbatim (frontmatter serialization is an exact inverse) and the body as
76
+ `html_to_md(live)`; `push` compares frontmatter parsed-value-equal and the body
77
+ in markdown space, so nothing differs. Caveats that keep this true:
78
+
79
+ - Bodies are diffed as markdown, not re-encoded HTML (so `md_to_html`/`html_to_md`
80
+ non-canonical drift never triggers a write). After a real body edit is pushed,
81
+ the server HTML is canonical and every later round-trip stays stable
82
+ (`md_to_html(html_to_md(md_to_html(x))) == md_to_html(x)`; see `markup.py`).
83
+ - `push` overlays onto the live object, so server-only keys absent from a file
84
+ (e.g. a new `personalDetails` field) are preserved, not diffed.
85
+ - The manifest `pulledAt` is **informational only** — there is no conflict
86
+ detection. If the server changed since your pull, `push` overwrites with your
87
+ local values (last-writer-wins); run `push --dry-run` first to review.
@@ -0,0 +1,13 @@
1
+ """flowcvcli — control a FlowCV resume from Python or the command line."""
2
+ from .api import FlowCV
3
+ from .config import Config
4
+ from .content import SECTION_META, label_of
5
+ from .errors import (ApiError, AuthError, FlowCVError, NotFoundError,
6
+ RateLimitError)
7
+ from .jsonresume import from_jsonresume, to_jsonresume
8
+ from .markup import html_to_md, html_to_text, md_to_html
9
+
10
+ __all__ = ["FlowCV", "Config", "SECTION_META", "label_of", "md_to_html", "html_to_text",
11
+ "html_to_md", "to_jsonresume", "from_jsonresume",
12
+ "FlowCVError", "AuthError", "RateLimitError", "NotFoundError", "ApiError"]
13
+ __version__ = "0.7.0"