flowcvcli 0.5.2__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 (53) hide show
  1. {flowcvcli-0.5.2/flowcvcli.egg-info → flowcvcli-0.7.0}/PKG-INFO +68 -14
  2. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/README.md +65 -12
  3. {flowcvcli-0.5.2 → 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.7.0/flowcvcli/client.py +490 -0
  8. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/config.py +56 -2
  9. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/content.py +39 -21
  10. {flowcvcli-0.5.2 → 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.5.2 → flowcvcli-0.7.0}/flowcvcli/personal.py +4 -2
  15. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/photo.py +4 -3
  16. flowcvcli-0.7.0/flowcvcli/repo.py +346 -0
  17. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/resume.py +61 -5
  18. {flowcvcli-0.5.2 → flowcvcli-0.7.0/flowcvcli.egg-info}/PKG-INFO +68 -14
  19. flowcvcli-0.7.0/flowcvcli.egg-info/SOURCES.txt +46 -0
  20. flowcvcli-0.7.0/flowcvcli.egg-info/requires.txt +1 -0
  21. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/pyproject.toml +5 -2
  22. flowcvcli-0.7.0/tests/test_batch.py +101 -0
  23. flowcvcli-0.7.0/tests/test_cli.py +72 -0
  24. flowcvcli-0.7.0/tests/test_client_auth.py +103 -0
  25. flowcvcli-0.7.0/tests/test_config.py +54 -0
  26. flowcvcli-0.7.0/tests/test_content.py +106 -0
  27. flowcvcli-0.7.0/tests/test_customize_cli.py +105 -0
  28. flowcvcli-0.7.0/tests/test_doctor.py +155 -0
  29. flowcvcli-0.7.0/tests/test_edit.py +113 -0
  30. flowcvcli-0.7.0/tests/test_errors.py +58 -0
  31. flowcvcli-0.7.0/tests/test_json_output.py +223 -0
  32. flowcvcli-0.7.0/tests/test_jsonresume.py +397 -0
  33. flowcvcli-0.7.0/tests/test_jsonresume_cli.py +99 -0
  34. flowcvcli-0.7.0/tests/test_markup.py +148 -0
  35. flowcvcli-0.7.0/tests/test_pullpush.py +359 -0
  36. flowcvcli-0.7.0/tests/test_ratelimit.py +89 -0
  37. flowcvcli-0.7.0/tests/test_resume.py +63 -0
  38. flowcvcli-0.7.0/tests/test_snapshots.py +157 -0
  39. flowcvcli-0.5.2/flowcvcli/__init__.py +0 -8
  40. flowcvcli-0.5.2/flowcvcli/cli.py +0 -405
  41. flowcvcli-0.5.2/flowcvcli/client.py +0 -300
  42. flowcvcli-0.5.2/flowcvcli/markup.py +0 -62
  43. flowcvcli-0.5.2/flowcvcli.egg-info/SOURCES.txt +0 -24
  44. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/.env.example +0 -0
  45. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/LICENSE +0 -0
  46. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/MANIFEST.in +0 -0
  47. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/docs/RENDERING.md +0 -0
  48. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/__main__.py +0 -0
  49. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/api.py +0 -0
  50. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli.egg-info/dependency_links.txt +0 -0
  51. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli.egg-info/entry_points.txt +0 -0
  52. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli.egg-info/top_level.txt +0 -0
  53. {flowcvcli-0.5.2 → flowcvcli-0.7.0}/setup.cfg +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowcvcli
3
- Version: 0.5.2
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. Zero dependencies.
3
+ Version: 0.7.0
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
7
7
  Project-URL: Homepage, https://github.com/dannyota/flowcvcli
@@ -26,6 +26,7 @@ Classifier: Topic :: Utilities
26
26
  Requires-Python: >=3.8
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
+ Requires-Dist: curl_cffi>=0.7
29
30
  Dynamic: license-file
30
31
 
31
32
  # flowcvcli
@@ -36,10 +37,14 @@ Dynamic: license-file
36
37
 
37
38
  Control a [FlowCV](https://flowcv.com) resume from the **command line** or from
38
39
  **Python** — content, header & links, **customization**, **templates**,
39
- **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.
40
44
  It drives FlowCV's private JSON API (the same calls the web app makes), so it
41
- works for any FlowCV resume with your own session. **Zero dependencies** (Python
42
- 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.
43
48
 
44
49
  > Unofficial and not affiliated with FlowCV. It uses FlowCV's undocumented
45
50
  > internal API and may break if that changes; use it with your own account and at
@@ -102,7 +107,8 @@ flowcv add work --set title="Engineer" --set company="Acme" \
102
107
  --set start=01/2022 --set end=Present --text $'- Did a measurable thing.'
103
108
  flowcv add custom2 --section-name "Open Source" --icon code --text "…" # heading+icon at creation
104
109
  flowcv desc work <id> --file role.md # rich text; --field is optional (auto: profile=text, skill=infoHtml)
105
- 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
106
112
  flowcv field work <id> employer --text "Acme Corp"
107
113
  flowcv rm work <id>
108
114
 
@@ -113,6 +119,7 @@ flowcv rename-section skill "Core Skills"
113
119
  flowcv section-icon skill head-side-brain
114
120
  flowcv rm-section custom1 --yes # delete a section + its entries
115
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
116
123
 
117
124
  # header details & links (links are social entries: orcid, googlescholar, github…)
118
125
  flowcv pd jobTitle --text "Security Leader"
@@ -124,26 +131,47 @@ flowcv avatar set https://example.com/me.png # upload from URL or file
124
131
  flowcv avatar on | off | remove
125
132
 
126
133
  # styling (a delta into resume.customization) and templates
127
- 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
128
137
  flowcv customize colors.basic.single '"#0e374e"'
138
+ flowcv icons # list commonly-used section iconKeys
129
139
  flowcv templates # lists each as [free] / [PAID] (paid needs a subscription)
130
140
  flowcv apply-template <templateId> # warns first if the template is paid
131
141
 
132
142
  # render & share
133
- flowcv download -o resume.pdf # the rendered PDF
143
+ flowcv download -o resume.pdf # the rendered PDF (--pages N caps rendered pages; default 10)
134
144
  flowcv download --token <webToken> -o out.pdf # any PUBLIC resume by its share token (no auth)
135
145
  flowcv share | publish | unpublish
136
146
 
137
147
  # backup / restore
138
148
  flowcv export -o backup.json # full resume snapshot (JSON) — keep one before big edits
139
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)
140
153
 
141
154
  flowcv login # refresh the cached session
155
+ flowcv doctor # diagnose auth/session/setup (--offline skips the live probe)
142
156
  flowcv md2html --file role.md # preview HTML (offline)
143
157
  ```
144
158
 
145
- Any command takes `--resume-id <id>` to target a specific resume. (From source,
146
- 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).
147
175
 
148
176
  ## Library (for scripts & LLM agents)
149
177
 
@@ -166,12 +194,29 @@ fc.rename_section("skill", "Core Skills"); fc.delete_section("custom1")
166
194
  fc.hide_entry("work", "id", hidden=True)
167
195
  new_id = fc.create_resume("Second Resume") # or fc.duplicate_resume()
168
196
  fc.rename_resume("New Title"); fc.delete_resume() # delete is permanent
169
- 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)
170
198
 
171
199
  # backup / restore
172
200
  import json
173
201
  json.dump(fc.export_resume(), open("backup.json", "w")) # full snapshot
174
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
175
220
  ```
176
221
 
177
222
  ### Build → render → check → improve
@@ -192,6 +237,10 @@ feedback loop for building a resume from raw info.
192
237
  | `**bold**` / `***bold-italic***` inline | `<strong>` / `<strong><em>…</em></strong>` |
193
238
  | `[text](url)` inline | link |
194
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
+
195
244
  ## How it works
196
245
 
197
246
  - **Read-modify-write**: edits fetch the resume, change one part, and send it
@@ -209,16 +258,21 @@ feedback loop for building a resume from raw info.
209
258
  ```
210
259
  flowcvcli/ # the package (import flowcvcli)
211
260
  config.py # resolve resume id + auth from .env / env vars
212
- client.py # HTTP, login, cookie-jar session, retry, get_resume
213
- 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)
214
264
  content.py # sections & entries (add/edit/reorder/hide/sections)
215
265
  personal.py # header details & links
216
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
217
269
  photo.py # avatar upload / toggle
218
- resume.py # list, create/duplicate/rename/delete, download, publish
270
+ resume.py # list, create/duplicate/rename/delete, download, publish, snapshots
219
271
  api.py # FlowCV = Client + all mixins
220
272
  cli.py / __main__.py # the `flowcv` command
273
+ tests/ # offline unit tests: python3 -m unittest discover -s tests -t .
221
274
  docs/API.md # reverse-engineered API reference
275
+ docs/PULLPUSH.md # resume-as-code design (layout, frontmatter, diff rules)
222
276
  docs/RENDERING.md # how the editor renders the preview & debounces saves
223
277
  flowcv.py # source-tree entry point (python3 flowcv.py …)
224
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"