linkfetch 0.1.3__tar.gz → 1.0.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 (54) hide show
  1. {linkfetch-0.1.3 → linkfetch-1.0.0}/PKG-INFO +2 -2
  2. {linkfetch-0.1.3 → linkfetch-1.0.0}/pyproject.toml +2 -2
  3. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/cli.py +18 -0
  4. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/emit.py +24 -1
  5. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/common.py +3 -0
  6. linkfetch-1.0.0/tests/test_regressions.py +34 -0
  7. {linkfetch-0.1.3 → linkfetch-1.0.0}/uv.lock +1 -1
  8. {linkfetch-0.1.3 → linkfetch-1.0.0}/.github/workflows/release.yml +0 -0
  9. {linkfetch-0.1.3 → linkfetch-1.0.0}/.github/workflows/test.yml +0 -0
  10. {linkfetch-0.1.3 → linkfetch-1.0.0}/.gitignore +0 -0
  11. {linkfetch-0.1.3 → linkfetch-1.0.0}/LICENSE +0 -0
  12. {linkfetch-0.1.3 → linkfetch-1.0.0}/README.md +0 -0
  13. {linkfetch-0.1.3 → linkfetch-1.0.0}/config.toml +0 -0
  14. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/__init__.py +0 -0
  15. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/capture/__init__.py +0 -0
  16. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/capture/browser.py +0 -0
  17. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/capture/expand.py +0 -0
  18. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/config.py +0 -0
  19. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/models.py +0 -0
  20. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/__init__.py +0 -0
  21. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/awards.py +0 -0
  22. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/basics.py +0 -0
  23. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/certifications.py +0 -0
  24. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/courses.py +0 -0
  25. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/education.py +0 -0
  26. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/experience.py +0 -0
  27. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/honors.py +0 -0
  28. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/languages.py +0 -0
  29. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/patents.py +0 -0
  30. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/projects.py +0 -0
  31. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/recommendations.py +0 -0
  32. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/skills.py +0 -0
  33. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/parse/volunteering.py +0 -0
  34. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/sections.py +0 -0
  35. {linkfetch-0.1.3 → linkfetch-1.0.0}/src/linkfetch/text.py +0 -0
  36. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/conftest.py +0 -0
  37. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/basics.html +0 -0
  38. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/certifications.html +0 -0
  39. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/education.html +0 -0
  40. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/experience.html +0 -0
  41. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/honors.html +0 -0
  42. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/languages.html +0 -0
  43. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/patents.html +0 -0
  44. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/projects.html +0 -0
  45. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/recommendations.html +0 -0
  46. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/skills.html +0 -0
  47. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/fixtures/volunteering.html +0 -0
  48. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/test_capture_retry.py +0 -0
  49. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/test_config.py +0 -0
  50. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/test_emit.py +0 -0
  51. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/test_experience.py +0 -0
  52. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/test_release.py +0 -0
  53. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/test_sections.py +0 -0
  54. {linkfetch-0.1.3 → linkfetch-1.0.0}/tests/test_text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: linkfetch
3
- Version: 0.1.3
3
+ Version: 1.0.0
4
4
  Summary: Capture your own LinkedIn profile on your own computer and turn it into structured profile files
5
5
  Project-URL: Homepage, https://github.com/Prosperis/linkfetch
6
6
  Project-URL: Issues, https://github.com/Prosperis/linkfetch/issues
@@ -8,7 +8,7 @@ Author: Prosperis
8
8
  License-Expression: MIT
9
9
  License-File: LICENSE
10
10
  Keywords: cv,export,linkedin,playwright,profile,resume
11
- Classifier: Development Status :: 4 - Beta
11
+ Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Intended Audience :: End Users/Desktop
14
14
  Classifier: Operating System :: OS Independent
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "linkfetch"
3
- version = "0.1.3"
3
+ version = "1.0.0"
4
4
  description = "Capture your own LinkedIn profile on your own computer and turn it into structured profile files"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -9,7 +9,7 @@ license-files = ["LICENSE"]
9
9
  authors = [{ name = "Prosperis" }]
10
10
  keywords = ["linkedin", "profile", "resume", "cv", "export", "playwright"]
11
11
  classifiers = [
12
- "Development Status :: 4 - Beta",
12
+ "Development Status :: 5 - Production/Stable",
13
13
  "Environment :: Console",
14
14
  "Intended Audience :: End Users/Desktop",
15
15
  "Operating System :: OS Independent",
@@ -52,6 +52,20 @@ def _playwright_installed() -> bool:
52
52
  return importlib.util.find_spec("playwright") is not None
53
53
 
54
54
 
55
+ def _browser_downloaded() -> bool:
56
+ """True if the Chromium build this Playwright version drives is on disk.
57
+
58
+ Starts Playwright's driver only to ask for the path; no browser opens.
59
+ """
60
+ try:
61
+ from playwright.sync_api import sync_playwright
62
+
63
+ with sync_playwright() as p:
64
+ return Path(p.chromium.executable_path).is_file()
65
+ except Exception:
66
+ return False
67
+
68
+
55
69
  def _section_arg(sections: list[str] | None) -> list[Section]:
56
70
  try:
57
71
  return resolve(sections)
@@ -76,6 +90,10 @@ def doctor() -> None:
76
90
 
77
91
  if _playwright_installed():
78
92
  console.print("[green]✓[/] Playwright is installed")
93
+ if _browser_downloaded():
94
+ console.print("[green]✓[/] The browser is downloaded")
95
+ else:
96
+ console.print("[yellow]![/] The browser is not downloaded yet — run `linkfetch install-browser`")
79
97
  else:
80
98
  console.print(
81
99
  "[yellow]![/] Playwright not installed — capture/login unavailable.\n"
@@ -24,7 +24,30 @@ def _to_plain(models: list[BaseModel] | BaseModel) -> object:
24
24
  """
25
25
  if isinstance(models, BaseModel):
26
26
  return _prune(models.model_dump())
27
- return [_prune(m.model_dump()) for m in models]
27
+ return _unique([_prune(m.model_dump()) for m in models])
28
+
29
+
30
+ def _unique(items: list[dict]) -> list[dict]:
31
+ """Drop exact duplicates and keep ``id`` values unique.
32
+
33
+ LinkedIn can render the same entry twice (a lazy-loaded list re-rendering,
34
+ or an entry genuinely listed twice); an identical copy adds nothing. Two
35
+ *different* entries that slug to the same ``id`` both stay, the later one
36
+ suffixed ``-2``, ``-3`` … so ids remain unique within a file.
37
+ """
38
+ out: list[dict] = []
39
+ seen_ids: dict[str, int] = {}
40
+ for item in items:
41
+ if item in out:
42
+ continue
43
+ base = item.get("id")
44
+ if isinstance(base, str):
45
+ count = seen_ids.get(base, 0) + 1
46
+ seen_ids[base] = count
47
+ if count > 1:
48
+ item = {**item, "id": f"{base}-{count}"}
49
+ out.append(item)
50
+ return out
28
51
 
29
52
 
30
53
  def _prune(value: object) -> object:
@@ -44,6 +44,9 @@ _CHROME = (
44
44
  "select language", "profile language", "accessibility", "talent solutions",
45
45
  "community guidelines", "marketing solutions", "privacy & terms",
46
46
  "ad choices", "sales solutions", "small business", "safety center",
47
+ # In-section notices, e.g. "Projects from connected apps sync
48
+ # automatically and can't be edited on LinkedIn."
49
+ "sync automatically", "be edited on linkedin",
47
50
  )
48
51
 
49
52
  # Section-title fragments that appear as the first child block of the content
@@ -0,0 +1,34 @@
1
+ """Bugs found in a real capture, kept fixed."""
2
+
3
+ from linkfetch.emit import _to_plain
4
+ from linkfetch.models import Course
5
+ from linkfetch.parse import projects
6
+
7
+
8
+ def _page(*entries: str) -> str:
9
+ return (
10
+ "<html><body><main><section aria-label='Primary content'>"
11
+ "<div><p>Projects</p></div><div>" + "".join(entries) + "</div>"
12
+ "</section></main></body></html>"
13
+ )
14
+
15
+
16
+ def test_linkedin_notice_is_not_a_project():
17
+ html = _page(
18
+ "<div><p>Projects from connected apps sync automatically and can’t be edited on LinkedIn.</p>"
19
+ "<span>Learn more</span></div>",
20
+ "<div><p>Example Viewer</p><p>Nov 2025 – Present</p></div>",
21
+ "<div><p>Example CLI</p><p>Jan 2024 – Mar 2024</p></div>",
22
+ )
23
+ assert [p.name for p in projects.parse(html)] == ["Example Viewer", "Example CLI"]
24
+
25
+
26
+ def test_identical_entries_are_written_once_and_ids_stay_unique():
27
+ data = _to_plain([
28
+ Course(id="stats-math-32", name="Probability and Statistics", number="MATH 32"),
29
+ Course(id="stats-math-32", name="Probability and Statistics", number="MATH 32"),
30
+ Course(id="intro", name="Intro", associated_with="School A"),
31
+ Course(id="intro", name="Intro", associated_with="School B"),
32
+ ])
33
+ assert [c["id"] for c in data] == ["stats-math-32", "intro", "intro-2"]
34
+ assert data[2]["associated_with"] == "School B"
@@ -138,7 +138,7 @@ wheels = [
138
138
 
139
139
  [[package]]
140
140
  name = "linkfetch"
141
- version = "0.1.3"
141
+ version = "1.0.0"
142
142
  source = { editable = "." }
143
143
  dependencies = [
144
144
  { name = "lxml" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes