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.
- {flowcvcli-0.5.2/flowcvcli.egg-info → flowcvcli-0.7.0}/PKG-INFO +68 -14
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/README.md +65 -12
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/docs/API.md +1 -1
- flowcvcli-0.7.0/docs/PULLPUSH.md +87 -0
- flowcvcli-0.7.0/flowcvcli/__init__.py +13 -0
- flowcvcli-0.7.0/flowcvcli/cli.py +811 -0
- flowcvcli-0.7.0/flowcvcli/client.py +490 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/config.py +56 -2
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/content.py +39 -21
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/customization.py +7 -5
- flowcvcli-0.7.0/flowcvcli/errors.py +29 -0
- flowcvcli-0.7.0/flowcvcli/jsonresume.py +350 -0
- flowcvcli-0.7.0/flowcvcli/markup.py +159 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/personal.py +4 -2
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/photo.py +4 -3
- flowcvcli-0.7.0/flowcvcli/repo.py +346 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/resume.py +61 -5
- {flowcvcli-0.5.2 → flowcvcli-0.7.0/flowcvcli.egg-info}/PKG-INFO +68 -14
- flowcvcli-0.7.0/flowcvcli.egg-info/SOURCES.txt +46 -0
- flowcvcli-0.7.0/flowcvcli.egg-info/requires.txt +1 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/pyproject.toml +5 -2
- flowcvcli-0.7.0/tests/test_batch.py +101 -0
- flowcvcli-0.7.0/tests/test_cli.py +72 -0
- flowcvcli-0.7.0/tests/test_client_auth.py +103 -0
- flowcvcli-0.7.0/tests/test_config.py +54 -0
- flowcvcli-0.7.0/tests/test_content.py +106 -0
- flowcvcli-0.7.0/tests/test_customize_cli.py +105 -0
- flowcvcli-0.7.0/tests/test_doctor.py +155 -0
- flowcvcli-0.7.0/tests/test_edit.py +113 -0
- flowcvcli-0.7.0/tests/test_errors.py +58 -0
- flowcvcli-0.7.0/tests/test_json_output.py +223 -0
- flowcvcli-0.7.0/tests/test_jsonresume.py +397 -0
- flowcvcli-0.7.0/tests/test_jsonresume_cli.py +99 -0
- flowcvcli-0.7.0/tests/test_markup.py +148 -0
- flowcvcli-0.7.0/tests/test_pullpush.py +359 -0
- flowcvcli-0.7.0/tests/test_ratelimit.py +89 -0
- flowcvcli-0.7.0/tests/test_resume.py +63 -0
- flowcvcli-0.7.0/tests/test_snapshots.py +157 -0
- flowcvcli-0.5.2/flowcvcli/__init__.py +0 -8
- flowcvcli-0.5.2/flowcvcli/cli.py +0 -405
- flowcvcli-0.5.2/flowcvcli/client.py +0 -300
- flowcvcli-0.5.2/flowcvcli/markup.py +0 -62
- flowcvcli-0.5.2/flowcvcli.egg-info/SOURCES.txt +0 -24
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/.env.example +0 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/LICENSE +0 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/MANIFEST.in +0 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/docs/RENDERING.md +0 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/__main__.py +0 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli/api.py +0 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli.egg-info/dependency_links.txt +0 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli.egg-info/entry_points.txt +0 -0
- {flowcvcli-0.5.2 → flowcvcli-0.7.0}/flowcvcli.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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,
|
|
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.
|
|
42
|
-
|
|
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
|
|
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
|
|
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
|
|
146
|
-
|
|
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 (
|
|
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,
|
|
213
|
-
|
|
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,
|
|
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.
|
|
12
|
-
|
|
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
|
|
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
|
|
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
|
|
116
|
-
|
|
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 (
|
|
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,
|
|
183
|
-
|
|
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"
|