flowcvcli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flowcvcli/resume.py ADDED
@@ -0,0 +1,122 @@
1
+ """Resume-level operations: listing, create/duplicate/rename/delete, PDF
2
+ download, web-resume publish/status.
3
+
4
+ ResumeMixin is a pure mixin over Client (see client.py): no __init__, all
5
+ state lives on `self`. Write methods return the JSON envelope dict so callers
6
+ can check `env["success"]`.
7
+ """
8
+ import json
9
+ import uuid
10
+
11
+ # Top-level resume fields that must NOT be copied into a new resume: server
12
+ # regenerates them (unique tokens / timestamps). `id` and `uuid` are reassigned.
13
+ _NEW_RESUME_DROP = ("webToken", "feedbackToken", "createdAt", "updatedAt", "lastChangeAt")
14
+
15
+
16
+ class ResumeMixin:
17
+ # ---- listing ----------------------------------------------------------
18
+ def list_resumes(self):
19
+ """GET /resumes/all -> the list of resume summaries. Raises on failure."""
20
+ env = self.request("resumes/all")
21
+ if not env.get("success"):
22
+ raise SystemExit(f"list resumes failed: {env}")
23
+ return env["data"]["resumes"]
24
+
25
+ # ---- create / duplicate / rename / delete -----------------------------
26
+ def _create_from(self, title, keep_content):
27
+ """Create a new resume by cloning the current one's full object.
28
+
29
+ FlowCV's `create` needs a complete resume object (every NOT-NULL column),
30
+ so we clone the current resume — guaranteeing validity — then reassign a
31
+ fresh id/uuid, drop the unique tokens (server regenerates), and set the
32
+ title. `keep_content=False` makes a blank resume that keeps the same
33
+ identity (personalDetails) and design (customization); `keep_content=True`
34
+ is a full duplicate. Returns the new resume id.
35
+ """
36
+ src = self.get_resume()
37
+ clone = json.loads(json.dumps(src)) # deep copy
38
+ new_id = str(uuid.uuid4())
39
+ clone["id"] = new_id
40
+ clone["uuid"] = str(uuid.uuid4())
41
+ clone["title"] = title
42
+ for k in _NEW_RESUME_DROP:
43
+ clone.pop(k, None)
44
+ if not keep_content:
45
+ clone["content"] = {}
46
+ env = self.request("resumes/create", method="POST", body={"clientResume": clone})
47
+ if not env.get("success"):
48
+ raise SystemExit(f"create resume failed: {json.dumps(env)[:200]}")
49
+ return new_id
50
+
51
+ def create_resume(self, title):
52
+ """Create a new, empty resume (same contact details & styling, no content).
53
+ Returns the new resume id."""
54
+ return self._create_from(title, keep_content=False)
55
+
56
+ def duplicate_resume(self, title=None):
57
+ """Duplicate the current resume (content and all). Returns the new id."""
58
+ if title is None:
59
+ title = (self.get_resume().get("title") or "Resume") + " (copy)"
60
+ return self._create_from(title, keep_content=True)
61
+
62
+ def rename_resume(self, title):
63
+ """PATCH /resumes/rename_resume — set the resume's title."""
64
+ return self.request("resumes/rename_resume", method="PATCH",
65
+ body={"resumeId": self.resume_id, "resumeTitle": title})
66
+
67
+ def delete_resume(self, resume_id=None):
68
+ """DELETE /resumes/delete_resume — permanently delete a resume.
69
+
70
+ Defaults to the configured resume; pass an explicit id to delete another.
71
+ Irreversible — the CLI guards this behind --yes.
72
+ """
73
+ rid = resume_id or self.resume_id
74
+ return self.request("resumes/delete_resume", method="DELETE", query={"resumeId": rid})
75
+
76
+ # ---- PDF --------------------------------------------------------------
77
+ def download_pdf(self, pages=10):
78
+ """GET /resumes/download -> PDF bytes. Raises unless a 200 + %PDF body."""
79
+ status, raw = self.request_raw(
80
+ "resumes/download",
81
+ query={"resumeId": self.resume_id, "previewPageCount": pages},
82
+ )
83
+ if status != 200 or not raw.startswith(b"%PDF"):
84
+ raise SystemExit(f"download failed: HTTP {status}, {raw[:80]!r}")
85
+ return raw
86
+
87
+ def save_pdf(self, path, pages=10):
88
+ """Download the resume PDF and write it to `path`; return `path`."""
89
+ with open(path, "wb") as f:
90
+ f.write(self.download_pdf(pages))
91
+ return path
92
+
93
+ def download_public(self, token):
94
+ """Download ANY public/shared resume's PDF by its web token (no ownership needed)."""
95
+ status, raw = self.request_raw("public/download_resume", query={"token": token})
96
+ if status != 200 or not raw.startswith(b"%PDF"):
97
+ raise SystemExit(f"public download failed: HTTP {status}, {raw[:80]!r}")
98
+ return raw
99
+
100
+ # ---- web resume -------------------------------------------------------
101
+ def publish(self):
102
+ """PATCH /resumes/publish_web_resume to make the web resume public."""
103
+ return self.request("resumes/publish_web_resume", method="PATCH",
104
+ body={"publish": True, "resumeId": self.resume_id})
105
+
106
+ def unpublish(self):
107
+ """PATCH /resumes/publish_web_resume to take the web resume offline."""
108
+ return self.request("resumes/publish_web_resume", method="PATCH",
109
+ body={"publish": False, "resumeId": self.resume_id})
110
+
111
+ def web_status(self):
112
+ """Return {live, url} for the public web resume (url None if no token)."""
113
+ r = self.get_resume()
114
+ token = r.get("webToken")
115
+ return {
116
+ "live": r.get("webResumeLive"),
117
+ "url": "https://flowcv.com/resume/" + token if token else None,
118
+ }
119
+
120
+ def share_url(self):
121
+ """Return the public web-resume URL (or None if no webToken)."""
122
+ return self.web_status()["url"]
@@ -0,0 +1,213 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowcvcli
3
+ Version: 0.2.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. Zero dependencies.
5
+ Author: dannyota
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/dannyota/flowcvcli
8
+ Project-URL: Repository, https://github.com/dannyota/flowcvcli
9
+ Project-URL: Documentation, https://github.com/dannyota/flowcvcli/blob/main/docs/API.md
10
+ Project-URL: Issues, https://github.com/dannyota/flowcvcli/issues
11
+ Keywords: flowcv,resume,cv,cli,resume-builder,json-api,automation
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Office/Business
25
+ Classifier: Topic :: Utilities
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Dynamic: license-file
30
+
31
+ # flowcvcli
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/flowcvcli.svg)](https://pypi.org/project/flowcvcli/)
34
+ [![Python](https://img.shields.io/pypi/pyversions/flowcvcli.svg)](https://pypi.org/project/flowcvcli/)
35
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
36
+
37
+ Control a [FlowCV](https://flowcv.com) resume from the **command line** or from
38
+ **Python** — content, header & links, **customization**, **templates**,
39
+ **avatar**, reorder/hide, multi-resume management, publish, and **PDF export**.
40
+ 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.
43
+
44
+ > Unofficial and not affiliated with FlowCV. It uses FlowCV's undocumented
45
+ > internal API and may break if that changes; use it with your own account and at
46
+ > your own risk (mind FlowCV's Terms of Service). See [`docs/API.md`](docs/API.md)
47
+ > for the reverse-engineered API and [`docs/RENDERING.md`](docs/RENDERING.md) for
48
+ > how the editor renders the live preview and persists edits.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install flowcvcli # installs the `flowcv` command
54
+ ```
55
+
56
+ Or run from source without installing:
57
+
58
+ ```bash
59
+ git clone https://github.com/dannyota/flowcvcli && cd flowcvcli
60
+ python3 flowcv.py --help # equivalent to the `flowcv` command
61
+ ```
62
+
63
+ ## Configure
64
+
65
+ Put a `.env` in the directory you run `flowcv` from (or in
66
+ `~/.config/flowcvcli/.env`). Real environment variables override it.
67
+
68
+ ```dotenv
69
+ # Auth — pick ONE:
70
+ FLOWCV_COOKIE=flowcvsidapp=s%3A... # your session cookie, OR
71
+ # FLOWCV_EMAIL=you@example.com # log in with credentials instead
72
+ # FLOWCV_PASSWORD=... # (session cached to ~/.config/flowcvcli/session)
73
+
74
+ # FLOWCV_RESUME_ID=... # optional; only if your account has several resumes
75
+ ```
76
+
77
+ - **Cookie**: DevTools → Application → Cookies → `app.flowcv.com` → copy the
78
+ `flowcvsidapp` value. That single cookie is the auth.
79
+ - **Credentials**: with `FLOWCV_EMAIL` + `FLOWCV_PASSWORD` the tool logs in and
80
+ caches the session (re-login is automatic when the cookie expires). The cache
81
+ is written `0600` to `~/.config/flowcvcli/session` (override with
82
+ `$FLOWCV_SESSION_FILE`).
83
+ - **Resume id** is optional: with one resume it's auto-selected; with several,
84
+ set `FLOWCV_RESUME_ID` or pass `--resume-id <id>` (run `flowcv resumes` to list).
85
+
86
+ ## CLI
87
+
88
+ ```bash
89
+ flowcv resumes # list resumes (id, title, share token)
90
+ flowcv show [section] # sections + entries (ids, labels, dates)
91
+ flowcv dump <section> <id> # one entry, fields + rich text
92
+
93
+ # manage resumes (multi-resume / paid plans)
94
+ flowcv new "My Second Resume" # new resume (same details+style, no content) -> prints id
95
+ flowcv duplicate ["Copy title"] # full copy of the current resume
96
+ flowcv rename "New Title" # rename the current resume
97
+ flowcv delete-resume --yes # permanent (refuses without --yes)
98
+
99
+ # content (markdown mini-format below); `add` creates the section if needed
100
+ flowcv add work --set title="Engineer" --set company="Acme" \
101
+ --set start=01/2022 --set end=Present --text $'- Did a measurable thing.'
102
+ flowcv desc work <id> --file role.md
103
+ flowcv field work <id> employer --text "Acme Corp"
104
+ flowcv rm work <id>
105
+
106
+ # reorder / hide / sections
107
+ flowcv reorder work <id3> <id1> <id2> # set entry order (all of the section's ids)
108
+ flowcv hide work <id> ; flowcv show-entry work <id>
109
+ flowcv rename-section skill "Core Skills"
110
+ flowcv section-icon skill head-side-brain
111
+ flowcv rm-section custom1 --yes # delete a section + its entries
112
+ flowcv reorder-sections profile work skill education # one-column order
113
+
114
+ # header details & links (links are social entries: orcid, googlescholar, github…)
115
+ flowcv pd jobTitle --text "Security Leader"
116
+ flowcv link orcid ORCID https://orcid.org/0000-0000-0000-0000
117
+ flowcv unlink orcid ; flowcv links
118
+
119
+ # avatar
120
+ flowcv avatar set https://example.com/me.png # upload from URL or file
121
+ flowcv avatar on | off | remove
122
+
123
+ # styling (a delta into resume.customization) and templates
124
+ flowcv customize font.fontFamily "Source Sans Pro"
125
+ flowcv customize colors.basic.single '"#0e374e"'
126
+ flowcv templates # lists each as [free] / [PAID] (paid needs a subscription)
127
+ flowcv apply-template <templateId> # warns first if the template is paid
128
+
129
+ # render & share
130
+ flowcv download -o resume.pdf # the rendered PDF
131
+ flowcv download --token <webToken> -o out.pdf # any PUBLIC resume by its share token (no auth)
132
+ flowcv share | publish | unpublish
133
+
134
+ flowcv login # refresh the cached session
135
+ flowcv md2html --file role.md # preview HTML (offline)
136
+ ```
137
+
138
+ Any command takes `--resume-id <id>` to target a specific resume. (From source,
139
+ replace `flowcv` with `python3 flowcv.py`.)
140
+
141
+ ## Library (for scripts & LLM agents)
142
+
143
+ ```python
144
+ from flowcvcli import FlowCV
145
+
146
+ fc = FlowCV() # or FlowCV(resume_id="...")
147
+ fc.set_personal_field("fullName", "Jane Doe")
148
+ fc.add_entry("work", sets={"jobTitle": "Engineer", "employer": "Acme",
149
+ "startDateNew": "01/2022", "endDateNew": "Present"},
150
+ md="- Shipped a thing with **measurable** impact.")
151
+ fc.set("font.fontFamily", "Source Sans Pro") # a customization delta
152
+ fc.set_photo("https://example.com/me.png") # avatar from URL
153
+ fc.apply_template("a3fb6c37-...") # a design from list_templates()
154
+ fc.save_pdf("resume.pdf") # render to PDF
155
+
156
+ # structure & resume management
157
+ fc.reorder_entries("work", ["id3", "id1", "id2"]) # set entry order
158
+ fc.rename_section("skill", "Core Skills"); fc.delete_section("custom1")
159
+ fc.hide_entry("work", "id", hidden=True)
160
+ new_id = fc.create_resume("Second Resume") # or fc.duplicate_resume()
161
+ fc.rename_resume("New Title"); fc.delete_resume() # delete is permanent
162
+ ```
163
+
164
+ ### Build → render → check → improve
165
+
166
+ The PDF *is* the rendered output. An agent can write content, `save_pdf(...)`,
167
+ **open the PDF to see the actual layout**, then adjust and re-render — a closed
168
+ feedback loop for building a resume from raw info.
169
+
170
+ ## Markdown mini-format (`desc` / `add`)
171
+
172
+ | You write | You get |
173
+ |---|---|
174
+ | blank line | block separator |
175
+ | `## Heading` / `**Whole line bold**` | bold subheader |
176
+ | `- item` | bullet (consecutive = one list) |
177
+ | anything else | justified paragraph |
178
+ | `**bold**` inline | `<strong>bold</strong>` |
179
+
180
+ ## How it works
181
+
182
+ - **Read-modify-write**: edits fetch the resume, change one part, and send it
183
+ back — unrelated fields are never touched.
184
+ - New entries append to the bottom of their section; use `reorder` to change order.
185
+ - The on-screen preview is client-side HTML; the **PDF download is a separate
186
+ server render** of the same data (details in [`docs/RENDERING.md`](docs/RENDERING.md)).
187
+
188
+ > **Scope:** this tool covers **resumes**. The same FlowCV account also has Cover
189
+ > Letters, Job Tracker, Email Signatures and Personal Websites (separate APIs —
190
+ > see `docs/API.md` "Other FlowCV products"); documented but not implemented here.
191
+
192
+ ## Project layout
193
+
194
+ ```
195
+ flowcvcli/ # the package (import flowcvcli)
196
+ config.py # resolve resume id + auth from .env / env vars
197
+ client.py # HTTP, login, cookie-jar session, retry, get_resume
198
+ markup.py # markdown <-> FlowCV rich-text HTML
199
+ content.py # sections & entries (add/edit/reorder/hide/sections)
200
+ personal.py # header details & links
201
+ customization.py # styling deltas & templates
202
+ photo.py # avatar upload / toggle
203
+ resume.py # list, create/duplicate/rename/delete, download, publish
204
+ api.py # FlowCV = Client + all mixins
205
+ cli.py / __main__.py # the `flowcv` command
206
+ docs/API.md # reverse-engineered API reference
207
+ docs/RENDERING.md # how the editor renders the preview & debounces saves
208
+ flowcv.py # source-tree entry point (python3 flowcv.py …)
209
+ ```
210
+
211
+ ## License
212
+
213
+ [MIT](LICENSE) © dannyota
@@ -0,0 +1,18 @@
1
+ flowcvcli/__init__.py,sha256=x-8Y0Y2l1JAsBFthqIMytVGwTs40a2N5Hqtyk7cyJYA,329
2
+ flowcvcli/__main__.py,sha256=vswlkf5GlOpZWRnJCMyo3nxdNHjfpV64MfTlpoyqg6k,140
3
+ flowcvcli/api.py,sha256=fsTgehgjBJjLKVs4l3Ph21M8Yp8kA0-MZIdofv3M3Ho,1335
4
+ flowcvcli/cli.py,sha256=z1quLav3fBlzlQPcuG9udoq-KEcYzecet6QckMG1oaE,12541
5
+ flowcvcli/client.py,sha256=mK6Tn2axUaR2C07aoeSr6Qo5VGl8qB-aoY3h4csqD2g,13130
6
+ flowcvcli/config.py,sha256=EC-et6QjOVwDc7zGK6Dr50uNWJ27iOmqFKjF7_gyR3Q,3246
7
+ flowcvcli/content.py,sha256=hEJWM2OHVa74WBwuS5tYV1p7czEU0Tw0WEA0hBropuc,8123
8
+ flowcvcli/customization.py,sha256=Kz34TZnyzhVT9iN-neRf0I7UMdP4Q2QPOUBZceFvaRo,3869
9
+ flowcvcli/markup.py,sha256=AFGb7YI-kZ_KQgM7qxVHFZ_24dwzv7qZ8SYh3fxrFGU,1785
10
+ flowcvcli/personal.py,sha256=IjXGqOWt7K7ZVoqYZTupe5zhoeWo0La_YiDpQ9JXhJU,2755
11
+ flowcvcli/photo.py,sha256=PERJtjhNKFgYftBsGNBEcokq8yZ0mmVkDiw9AVHLVcs,4088
12
+ flowcvcli/resume.py,sha256=ZpI1gqwXE6y8WW0NVHm5l4cXj9fH2UTnxMqUOFXSg0Q,5564
13
+ flowcvcli-0.2.0.dist-info/licenses/LICENSE,sha256=23m7cKsJg9PqqLZ0wK5RbcEGNAFbSchahsBJ8LGvX0g,1065
14
+ flowcvcli-0.2.0.dist-info/METADATA,sha256=_yRE9G2QHWgy32PVmnSUjovBkYdsU2XGbRbJH0oU6js,9549
15
+ flowcvcli-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
+ flowcvcli-0.2.0.dist-info/entry_points.txt,sha256=rS7BgaYFbAad6mzLcqK62k4kMgZ4UeurHj5wT_rgCDk,46
17
+ flowcvcli-0.2.0.dist-info/top_level.txt,sha256=kvOYoW_loF42cvdHgkdKjyCQ-FiYDu1PLhAb40HkBaI,10
18
+ flowcvcli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flowcv = flowcvcli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dannyota
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ flowcvcli