recon-patent 0.2.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 (70) hide show
  1. recon_patent-0.2.0/LICENSE +21 -0
  2. recon_patent-0.2.0/PKG-INFO +232 -0
  3. recon_patent-0.2.0/README.md +189 -0
  4. recon_patent-0.2.0/cli/__init__.py +0 -0
  5. recon_patent-0.2.0/cli/download.py +35 -0
  6. recon_patent-0.2.0/cli/export.py +165 -0
  7. recon_patent-0.2.0/cli/main.py +305 -0
  8. recon_patent-0.2.0/clients/__init__.py +0 -0
  9. recon_patent-0.2.0/clients/base.py +56 -0
  10. recon_patent-0.2.0/clients/intelligence.py +101 -0
  11. recon_patent-0.2.0/clients/patent_apis.py +355 -0
  12. recon_patent-0.2.0/clients/scrapers.py +384 -0
  13. recon_patent-0.2.0/core/__init__.py +0 -0
  14. recon_patent-0.2.0/core/arbitrage.py +41 -0
  15. recon_patent-0.2.0/core/citations.py +197 -0
  16. recon_patent-0.2.0/core/config.py +87 -0
  17. recon_patent-0.2.0/core/intelligence.py +58 -0
  18. recon_patent-0.2.0/core/models.py +42 -0
  19. recon_patent-0.2.0/core/scoring.py +94 -0
  20. recon_patent-0.2.0/core/search.py +82 -0
  21. recon_patent-0.2.0/core/translation.py +126 -0
  22. recon_patent-0.2.0/pyproject.toml +62 -0
  23. recon_patent-0.2.0/recon_patent.egg-info/PKG-INFO +232 -0
  24. recon_patent-0.2.0/recon_patent.egg-info/SOURCES.txt +68 -0
  25. recon_patent-0.2.0/recon_patent.egg-info/dependency_links.txt +1 -0
  26. recon_patent-0.2.0/recon_patent.egg-info/entry_points.txt +2 -0
  27. recon_patent-0.2.0/recon_patent.egg-info/requires.txt +21 -0
  28. recon_patent-0.2.0/recon_patent.egg-info/top_level.txt +5 -0
  29. recon_patent-0.2.0/setup.cfg +4 -0
  30. recon_patent-0.2.0/storage/__init__.py +0 -0
  31. recon_patent-0.2.0/storage/cache.py +131 -0
  32. recon_patent-0.2.0/tests/test_arbitrage.py +44 -0
  33. recon_patent-0.2.0/tests/test_cache.py +53 -0
  34. recon_patent-0.2.0/tests/test_cache_validation.py +617 -0
  35. recon_patent-0.2.0/tests/test_citations.py +158 -0
  36. recon_patent-0.2.0/tests/test_claims_lazy_load.py +42 -0
  37. recon_patent-0.2.0/tests/test_cli_run.py +43 -0
  38. recon_patent-0.2.0/tests/test_client.py +25 -0
  39. recon_patent-0.2.0/tests/test_error_handling.py +926 -0
  40. recon_patent-0.2.0/tests/test_error_voice.py +55 -0
  41. recon_patent-0.2.0/tests/test_export.py +75 -0
  42. recon_patent-0.2.0/tests/test_imports.py +97 -0
  43. recon_patent-0.2.0/tests/test_integration_new.py +61 -0
  44. recon_patent-0.2.0/tests/test_intelligence.py +59 -0
  45. recon_patent-0.2.0/tests/test_lazy_loading.py +32 -0
  46. recon_patent-0.2.0/tests/test_models.py +21 -0
  47. recon_patent-0.2.0/tests/test_module_entrypoint.py +15 -0
  48. recon_patent-0.2.0/tests/test_patent_apis.py +94 -0
  49. recon_patent-0.2.0/tests/test_performance.py +964 -0
  50. recon_patent-0.2.0/tests/test_score_algorithm.py +129 -0
  51. recon_patent-0.2.0/tests/test_scoring.py +99 -0
  52. recon_patent-0.2.0/tests/test_search.py +97 -0
  53. recon_patent-0.2.0/tests/test_security.py +76 -0
  54. recon_patent-0.2.0/tests/test_source_filter.py +104 -0
  55. recon_patent-0.2.0/tests/test_tab_integration.py +180 -0
  56. recon_patent-0.2.0/tests/test_terminal_detection.py +31 -0
  57. recon_patent-0.2.0/tests/test_terminal_protocols.py +20 -0
  58. recon_patent-0.2.0/tests/test_translation.py +113 -0
  59. recon_patent-0.2.0/tests/test_tui_components.py +167 -0
  60. recon_patent-0.2.0/tests/test_tui_layout.py +128 -0
  61. recon_patent-0.2.0/tests/test_tui_navigation.py +120 -0
  62. recon_patent-0.2.0/tui/__init__.py +0 -0
  63. recon_patent-0.2.0/tui/app.py +221 -0
  64. recon_patent-0.2.0/tui/screens.py +978 -0
  65. recon_patent-0.2.0/tui/widgets/__init__.py +0 -0
  66. recon_patent-0.2.0/tui/widgets/citation_tree.py +76 -0
  67. recon_patent-0.2.0/tui/widgets/claims_tab.py +56 -0
  68. recon_patent-0.2.0/tui/widgets/image_tab.py +171 -0
  69. recon_patent-0.2.0/tui/widgets/info_tab.py +94 -0
  70. recon_patent-0.2.0/tui/widgets/result_list.py +44 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 RECON Contributors
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,232 @@
1
+ Metadata-Version: 2.4
2
+ Name: recon-patent
3
+ Version: 0.2.0
4
+ Summary: Terminal-native patent research CLI/TUI tool
5
+ Author-email: Anubhav Anand <anubhav@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/anubhavanand/recon
8
+ Project-URL: Repository, https://github.com/anubhavanand/recon
9
+ Project-URL: Documentation, https://github.com/anubhavanand/recon#readme
10
+ Project-URL: Issues, https://github.com/anubhavanand/recon/issues
11
+ Keywords: patent,research,cli,tui,textual
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.12
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: textual>=1.0
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: Pillow>=10.0
26
+ Requires-Dist: rapidfuzz>=3.0
27
+ Requires-Dist: typer>=0.9
28
+ Requires-Dist: fpdf2>=2.7
29
+ Requires-Dist: beautifulsoup4>=4.12
30
+ Requires-Dist: lxml>=5.0
31
+ Requires-Dist: ddgs>=1.0
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest>=8.0; extra == "test"
34
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
35
+ Requires-Dist: pytest-cov>=5.0; extra == "test"
36
+ Requires-Dist: psutil>=5.9; extra == "test"
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=8.0; extra == "dev"
39
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
40
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
41
+ Requires-Dist: psutil>=5.9; extra == "dev"
42
+ Dynamic: license-file
43
+
44
+ # RECON — Terminal-Native Patent Research Tool
45
+
46
+ **RECON** is a keyboard-first, terminal-native patent research tool. It aggregates patent data from multiple sources (USPTO, PatSnap, Google Patents, WIPO, Lens.org, EPO), presents results in a clean TUI or CLI, and provides scoring, citation graphs, export, and optional local-AI translation.
47
+
48
+ ## Features
49
+
50
+ - **Multi-source search** — USPTO API, PatSnap API, Google Patents scraping, WIPO/Lens/EPO via DuckDuckGo + BeautifulSoup
51
+ - **Source filtering** — Include/exclude sources via CLI `--source` or TUI `S` overlay
52
+ - **Signal scoring** — Equal-weight 20-point signals (government grants, corporate investment, academic research, temporal recency, news/media)
53
+ - **Live preview** — Three-tab detail pane (Info / Claims / Image) with keyboard navigation
54
+ - **Citation graph** — ASCII tree view of forward/backward citations (scraped from Google Patents or mock data)
55
+ - **Export** — JSON, CSV, BibTeX, Markdown, PDF
56
+ - **Translation** — Optional local translation via Ollama (opt-in, Zero-AI default)
57
+ - **Terminal-native** — No GUI, no Electron, no modal dialogs
58
+
59
+ ## Installation
60
+
61
+ ### Via PyPI (recommended)
62
+
63
+ ```bash
64
+ pipx install recon-patent
65
+ ```
66
+
67
+ ### Via GitHub
68
+
69
+ ```bash
70
+ pipx install git+https://github.com/anubhavaanand/recon.git
71
+ ```
72
+
73
+ Ensure `~/.local/bin` is in your `PATH`.
74
+
75
+ ### Via install script (hacker method)
76
+
77
+ ```bash
78
+ curl -sSL https://raw.githubusercontent.com/anubhavaanand/recon/main/install.sh | bash
79
+ ```
80
+
81
+ ### Via pip
82
+
83
+ ```bash
84
+ pip install recon-patent
85
+ ```
86
+
87
+ ### From source
88
+
89
+ ```bash
90
+ git clone https://github.com/anubhavaanand/recon.git
91
+ cd recon
92
+ pip install -e .
93
+ ```
94
+
95
+ ### Development
96
+
97
+ ```bash
98
+ pip install -e ".[test,dev]"
99
+ ```
100
+
101
+ ## Quick Start
102
+
103
+ ### Launch the interactive TUI
104
+
105
+ ```bash
106
+ recon
107
+ ```
108
+
109
+ Type a query (e.g. `solid state battery`) and press Enter. Navigate results with `j`/`k`, open detail with Enter, switch tabs with `h`/`l`.
110
+
111
+ ### CLI search
112
+
113
+ ```bash
114
+ recon search "sulfide electrolyte"
115
+ ```
116
+
117
+ Filter by source:
118
+
119
+ ```bash
120
+ recon search "quantum battery" --source uspto,patsnap,google
121
+ ```
122
+
123
+ ### End-to-end run with export
124
+
125
+ ```bash
126
+ recon run "lithium anode" --export json
127
+ ```
128
+
129
+ ### View saved collection
130
+
131
+ ```bash
132
+ recon collection list
133
+ recon collection clear
134
+ ```
135
+
136
+ ### Manage API keys
137
+
138
+ ```bash
139
+ recon config show
140
+ recon config set --patsnap-key YOUR_KEY
141
+ recon config test
142
+ ```
143
+
144
+ ### Export collection
145
+
146
+ ```bash
147
+ recon export --format csv
148
+ ```
149
+
150
+ Formats: `json`, `csv`, `bibtex`, `markdown`, `pdf`.
151
+
152
+ ## TUI Key Bindings
153
+
154
+ | Key | Action |
155
+ |-----|--------|
156
+ | `↑`/`↓` `j`/`k` | Navigate results |
157
+ | `Enter` | Open detail view |
158
+ | `h`/`l` or `←`/`→` | Switch preview tab |
159
+ | `/` | Focus search input |
160
+ | `s` | Save patent to collection |
161
+ | `e` | Export collection overlay |
162
+ | `S` | Source filter overlay |
163
+ | `c` | Toggle citation graph |
164
+ | `t` | Toggle translation |
165
+ | `r` | Reader mode |
166
+ | `m` | Synthesis mode |
167
+ | `?` | Help overlay |
168
+ | `q` / `Esc` | Back / Quit |
169
+
170
+ ## Architecture
171
+
172
+ ```
173
+ recon/
174
+ ├── cli/main.py — Typer CLI (search, run, config, export, collection)
175
+ ├── core/
176
+ │ ├── models.py — PatentRecord, CrossReference dataclasses
177
+ │ ├── search.py — Multi-source search orchestration + source filtering
178
+ │ ├── scoring.py — Signal scoring (equal-weight algorithm)
179
+ │ ├── arbitrage.py — Arbitrage status calculation
180
+ │ ├── citations.py — Citation graph fetching (Google Patents scrape + mock)
181
+ │ ├── translation.py — Local Ollama translation with cache
182
+ │ └── config.py — Config management (.env + JSON)
183
+ ├── clients/
184
+ │ ├── patent_apis.py — USPTO, PatSnap, Google, WIPO, Lens, EPO clients
185
+ │ ├── scrapers.py — DDGS + BeautifulSoup scrapers (WIPO, Lens, EPO, Google)
186
+ │ └── base.py — BaseAsyncClient with rate-limit + backoff
187
+ ├── tui/
188
+ │ ├── app.py — Textual App + CSS
189
+ │ ├── screens.py — SearchScreen, DetailScreen, ReaderModeScreen, etc.
190
+ │ └── widgets/ — ResultList, InfoTab, ClaimsTab, ImageTab, CitationTree
191
+ ├── storage/
192
+ │ └── cache.py — SQLite cache (search results, collection, translations)
193
+ └── tests/ — 200+ tests (pytest)
194
+ ```
195
+
196
+ ## Configuration
197
+
198
+ Keys are stored in `~/.config/recon/config.json` (chmod 600):
199
+
200
+ | Variable | Source | Required |
201
+ |----------|--------|----------|
202
+ | `PATSNAP_API_KEY` | PatSnap | No (scraper fallback exists for most features) |
203
+ | `USPTO_API_KEY` | USPTO | No |
204
+ | `EPO_CONSUMER_KEY` / `EPO_CONSUMER_SECRET` | EPO | No (scraper-only) |
205
+ | `LENS_API_KEY` | Lens.org | No (scraper-only) |
206
+
207
+ Set via `recon config set` (interactive) or `.env` file.
208
+
209
+ ## Translation (Ollama)
210
+
211
+ RECON can optionally translate non-English patent abstracts using a local Ollama instance:
212
+
213
+ ```bash
214
+ # Install Ollama: https://ollama.ai
215
+ ollama pull llama3
216
+
217
+ # RECON auto-detects non-English text and uses Ollama
218
+ # Press t in the TUI to toggle translation
219
+ ```
220
+
221
+ When Ollama is not running, RECON gracefully displays the original text.
222
+
223
+ ## Testing
224
+
225
+ ```bash
226
+ pip install -e ".[test]"
227
+ pytest
228
+ ```
229
+
230
+ ## License
231
+
232
+ MIT
@@ -0,0 +1,189 @@
1
+ # RECON — Terminal-Native Patent Research Tool
2
+
3
+ **RECON** is a keyboard-first, terminal-native patent research tool. It aggregates patent data from multiple sources (USPTO, PatSnap, Google Patents, WIPO, Lens.org, EPO), presents results in a clean TUI or CLI, and provides scoring, citation graphs, export, and optional local-AI translation.
4
+
5
+ ## Features
6
+
7
+ - **Multi-source search** — USPTO API, PatSnap API, Google Patents scraping, WIPO/Lens/EPO via DuckDuckGo + BeautifulSoup
8
+ - **Source filtering** — Include/exclude sources via CLI `--source` or TUI `S` overlay
9
+ - **Signal scoring** — Equal-weight 20-point signals (government grants, corporate investment, academic research, temporal recency, news/media)
10
+ - **Live preview** — Three-tab detail pane (Info / Claims / Image) with keyboard navigation
11
+ - **Citation graph** — ASCII tree view of forward/backward citations (scraped from Google Patents or mock data)
12
+ - **Export** — JSON, CSV, BibTeX, Markdown, PDF
13
+ - **Translation** — Optional local translation via Ollama (opt-in, Zero-AI default)
14
+ - **Terminal-native** — No GUI, no Electron, no modal dialogs
15
+
16
+ ## Installation
17
+
18
+ ### Via PyPI (recommended)
19
+
20
+ ```bash
21
+ pipx install recon-patent
22
+ ```
23
+
24
+ ### Via GitHub
25
+
26
+ ```bash
27
+ pipx install git+https://github.com/anubhavaanand/recon.git
28
+ ```
29
+
30
+ Ensure `~/.local/bin` is in your `PATH`.
31
+
32
+ ### Via install script (hacker method)
33
+
34
+ ```bash
35
+ curl -sSL https://raw.githubusercontent.com/anubhavaanand/recon/main/install.sh | bash
36
+ ```
37
+
38
+ ### Via pip
39
+
40
+ ```bash
41
+ pip install recon-patent
42
+ ```
43
+
44
+ ### From source
45
+
46
+ ```bash
47
+ git clone https://github.com/anubhavaanand/recon.git
48
+ cd recon
49
+ pip install -e .
50
+ ```
51
+
52
+ ### Development
53
+
54
+ ```bash
55
+ pip install -e ".[test,dev]"
56
+ ```
57
+
58
+ ## Quick Start
59
+
60
+ ### Launch the interactive TUI
61
+
62
+ ```bash
63
+ recon
64
+ ```
65
+
66
+ Type a query (e.g. `solid state battery`) and press Enter. Navigate results with `j`/`k`, open detail with Enter, switch tabs with `h`/`l`.
67
+
68
+ ### CLI search
69
+
70
+ ```bash
71
+ recon search "sulfide electrolyte"
72
+ ```
73
+
74
+ Filter by source:
75
+
76
+ ```bash
77
+ recon search "quantum battery" --source uspto,patsnap,google
78
+ ```
79
+
80
+ ### End-to-end run with export
81
+
82
+ ```bash
83
+ recon run "lithium anode" --export json
84
+ ```
85
+
86
+ ### View saved collection
87
+
88
+ ```bash
89
+ recon collection list
90
+ recon collection clear
91
+ ```
92
+
93
+ ### Manage API keys
94
+
95
+ ```bash
96
+ recon config show
97
+ recon config set --patsnap-key YOUR_KEY
98
+ recon config test
99
+ ```
100
+
101
+ ### Export collection
102
+
103
+ ```bash
104
+ recon export --format csv
105
+ ```
106
+
107
+ Formats: `json`, `csv`, `bibtex`, `markdown`, `pdf`.
108
+
109
+ ## TUI Key Bindings
110
+
111
+ | Key | Action |
112
+ |-----|--------|
113
+ | `↑`/`↓` `j`/`k` | Navigate results |
114
+ | `Enter` | Open detail view |
115
+ | `h`/`l` or `←`/`→` | Switch preview tab |
116
+ | `/` | Focus search input |
117
+ | `s` | Save patent to collection |
118
+ | `e` | Export collection overlay |
119
+ | `S` | Source filter overlay |
120
+ | `c` | Toggle citation graph |
121
+ | `t` | Toggle translation |
122
+ | `r` | Reader mode |
123
+ | `m` | Synthesis mode |
124
+ | `?` | Help overlay |
125
+ | `q` / `Esc` | Back / Quit |
126
+
127
+ ## Architecture
128
+
129
+ ```
130
+ recon/
131
+ ├── cli/main.py — Typer CLI (search, run, config, export, collection)
132
+ ├── core/
133
+ │ ├── models.py — PatentRecord, CrossReference dataclasses
134
+ │ ├── search.py — Multi-source search orchestration + source filtering
135
+ │ ├── scoring.py — Signal scoring (equal-weight algorithm)
136
+ │ ├── arbitrage.py — Arbitrage status calculation
137
+ │ ├── citations.py — Citation graph fetching (Google Patents scrape + mock)
138
+ │ ├── translation.py — Local Ollama translation with cache
139
+ │ └── config.py — Config management (.env + JSON)
140
+ ├── clients/
141
+ │ ├── patent_apis.py — USPTO, PatSnap, Google, WIPO, Lens, EPO clients
142
+ │ ├── scrapers.py — DDGS + BeautifulSoup scrapers (WIPO, Lens, EPO, Google)
143
+ │ └── base.py — BaseAsyncClient with rate-limit + backoff
144
+ ├── tui/
145
+ │ ├── app.py — Textual App + CSS
146
+ │ ├── screens.py — SearchScreen, DetailScreen, ReaderModeScreen, etc.
147
+ │ └── widgets/ — ResultList, InfoTab, ClaimsTab, ImageTab, CitationTree
148
+ ├── storage/
149
+ │ └── cache.py — SQLite cache (search results, collection, translations)
150
+ └── tests/ — 200+ tests (pytest)
151
+ ```
152
+
153
+ ## Configuration
154
+
155
+ Keys are stored in `~/.config/recon/config.json` (chmod 600):
156
+
157
+ | Variable | Source | Required |
158
+ |----------|--------|----------|
159
+ | `PATSNAP_API_KEY` | PatSnap | No (scraper fallback exists for most features) |
160
+ | `USPTO_API_KEY` | USPTO | No |
161
+ | `EPO_CONSUMER_KEY` / `EPO_CONSUMER_SECRET` | EPO | No (scraper-only) |
162
+ | `LENS_API_KEY` | Lens.org | No (scraper-only) |
163
+
164
+ Set via `recon config set` (interactive) or `.env` file.
165
+
166
+ ## Translation (Ollama)
167
+
168
+ RECON can optionally translate non-English patent abstracts using a local Ollama instance:
169
+
170
+ ```bash
171
+ # Install Ollama: https://ollama.ai
172
+ ollama pull llama3
173
+
174
+ # RECON auto-detects non-English text and uses Ollama
175
+ # Press t in the TUI to toggle translation
176
+ ```
177
+
178
+ When Ollama is not running, RECON gracefully displays the original text.
179
+
180
+ ## Testing
181
+
182
+ ```bash
183
+ pip install -e ".[test]"
184
+ pytest
185
+ ```
186
+
187
+ ## License
188
+
189
+ MIT
File without changes
@@ -0,0 +1,35 @@
1
+ import httpx
2
+ import os
3
+ from pathlib import Path
4
+ from core.models import PatentRecord
5
+
6
+ async def download_patent_assets(record: PatentRecord, base_path: str = "downloads"):
7
+ """
8
+ Download patent figures and metadata for offline archival.
9
+ PRD §3.5 — Download (key: d).
10
+ """
11
+ target_dir = Path(base_path) / record.id
12
+ target_dir.mkdir(parents=True, exist_ok=True)
13
+
14
+ # 1. Save metadata as JSON
15
+ import json
16
+ import dataclasses
17
+ meta_path = target_dir / "metadata.json"
18
+ with open(meta_path, "w") as f:
19
+ json.dump(dataclasses.asdict(record), f, indent=2)
20
+
21
+ # 2. Download figures
22
+ if record.image_urls:
23
+ async with httpx.AsyncClient(timeout=30.0, trust_env=False) as client:
24
+ for i, url in enumerate(record.image_urls, 1):
25
+ try:
26
+ response = await client.get(url)
27
+ if response.status_code == 200:
28
+ ext = Path(url).suffix or ".jpg"
29
+ img_path = target_dir / f"figure_{i}{ext}"
30
+ with open(img_path, "wb") as f:
31
+ f.write(response.content)
32
+ except Exception as e:
33
+ print(f"ERR: Failed to download figure {i}: {e}")
34
+
35
+ return target_dir
@@ -0,0 +1,165 @@
1
+ import csv
2
+ import json
3
+ from pathlib import Path
4
+ from typing import List, Any
5
+ from core.models import PatentRecord
6
+ import fpdf
7
+
8
+ def _safe_csv_field(value: str) -> str:
9
+ """Sanitize a CSV field to prevent formula injection in spreadsheet apps.
10
+
11
+ Leading =, +, -, @ can trigger DDE/formula execution in Excel/Google Sheets.
12
+ Prefix them with a tab to neutralize.
13
+ """
14
+ value = str(value)
15
+ if value and value[0] in ("=", "+", "-", "@", "\t", "\r"):
16
+ return "\t" + value
17
+ return value
18
+
19
+ def _export_csv(records: List[PatentRecord], output_path: str):
20
+ if not records:
21
+ Path(output_path).touch()
22
+ return
23
+
24
+ with open(output_path, mode='w', newline='', encoding='utf-8') as f:
25
+ writer = csv.writer(f)
26
+ writer.writerow(["ID", "Title", "Assignee", "Filed Date", "Status", "Score"])
27
+ from core.scoring import calculate_signal_score
28
+ for record in records:
29
+ score = calculate_signal_score(record.cross_references)
30
+ writer.writerow([
31
+ _safe_csv_field(record.id),
32
+ _safe_csv_field(record.title),
33
+ _safe_csv_field(record.assignee),
34
+ _safe_csv_field(record.dates.get("filed", "")),
35
+ _safe_csv_field(record.status),
36
+ _safe_csv_field(str(score))
37
+ ])
38
+
39
+ def _export_json(records: List[PatentRecord], output_path: str):
40
+ import dataclasses
41
+ def _default(o: Any):
42
+ if dataclasses.is_dataclass(o):
43
+ return dataclasses.asdict(o)
44
+ return str(o)
45
+
46
+ with open(output_path, 'w', encoding='utf-8') as f:
47
+ json.dump([_default(r) for r in records], f, indent=2)
48
+
49
+ def _export_bibtex(records: List[PatentRecord], output_path: str):
50
+ with open(output_path, 'w', encoding='utf-8') as f:
51
+ for record in records:
52
+ f.write(f"@misc{{{record.id},\n")
53
+ f.write(f" title = {{{record.title}}},\n")
54
+ f.write(f" author = {{{record.assignee}}},\n")
55
+ f.write(f" year = {{{record.dates.get('filed', '')[:4] if record.dates.get('filed') else ''}}},\n")
56
+ f.write(f" note = {{Status: {record.status}}}\n")
57
+ f.write("}\n\n")
58
+
59
+ def _export_markdown(records: List[PatentRecord], output_path: str):
60
+ with open(output_path, 'w', encoding='utf-8') as f:
61
+ for record in records:
62
+ f.write(f"# {record.title}\n\n")
63
+ f.write(f"**ID**: {record.id} | **Assignee**: {record.assignee} | **Filed**: {record.dates.get('filed', '[?]')}\n\n")
64
+ f.write(f"## Abstract\n{record.abstract}\n\n")
65
+ f.write("---\n\n")
66
+
67
+ def _export_pdf(records: List[PatentRecord], output_path: str):
68
+ """Generate PDF export with title page and individual patent pages."""
69
+ pdf = fpdf.FPDF()
70
+ pdf.set_auto_page_break(auto=True, margin=15)
71
+
72
+ # Title page
73
+ pdf.add_page()
74
+ pdf.set_font("helvetica", style="B", size=24)
75
+ pdf.ln(80)
76
+ pdf.cell(0, 20, "RECON Export", align="C")
77
+ pdf.ln(15)
78
+ pdf.set_font("helvetica", size=18)
79
+ pdf.cell(0, 20, f"{len(records)} Patents", align="C")
80
+ pdf.set_font("helvetica", size=12)
81
+ pdf.ln(50)
82
+ pdf.cell(0, 10, f"Generated: {Path(output_path).stem}", align="C")
83
+
84
+ # Patent pages
85
+ for record in records:
86
+ pdf.add_page()
87
+ pdf.set_font("helvetica", style="B", size=14)
88
+ safe_title = record.title.encode('latin-1', 'replace').decode('latin-1')
89
+ pdf.multi_cell(w=180, h=8, text=safe_title, align="L")
90
+ pdf.ln(3)
91
+
92
+ # ID, Assignee, Dates row
93
+ pdf.set_font("helvetica", size=10)
94
+ pdf.multi_cell(w=180, h=6, text=f"ID: {record.id}", align="L")
95
+ safe_assignee = record.assignee.encode('latin-1', 'replace').decode('latin-1')
96
+ pdf.multi_cell(w=180, h=6, text=f"Assignee: {safe_assignee}", align="L")
97
+
98
+ # Dates
99
+ dates_str = " | ".join([f"{k}: {v}" for k, v in record.dates.items()])
100
+ safe_dates = dates_str.encode('latin-1', 'replace').decode('latin-1')
101
+ pdf.multi_cell(w=180, h=6, text=safe_dates, align="L")
102
+ pdf.multi_cell(w=180, h=6, text=f"Status: {record.status}", align="L")
103
+ pdf.ln(5)
104
+
105
+ # Abstract
106
+ pdf.set_font("helvetica", style="B", size=11)
107
+ pdf.cell(0, 8, "Abstract", align="L")
108
+ pdf.ln(8)
109
+ pdf.set_font("helvetica", size=10)
110
+ safe_abstract = record.abstract.encode('latin-1', 'replace').decode('latin-1')
111
+ pdf.multi_cell(w=180, h=6, text=safe_abstract, align="L")
112
+ pdf.ln(5)
113
+
114
+ # Claims
115
+ if record.claims:
116
+ pdf.set_font("helvetica", style="B", size=11)
117
+ pdf.cell(0, 8, "Claims", align="L")
118
+ pdf.ln(8)
119
+ pdf.set_font("helvetica", size=10)
120
+ for i, claim in enumerate(record.claims, 1):
121
+ safe_claim = claim.encode('latin-1', 'replace').decode('latin-1')
122
+ claim_text = f"{i}. {safe_claim}"
123
+ pdf.multi_cell(w=180, h=6, text=claim_text, align="L")
124
+
125
+ pdf.output(output_path)
126
+
127
+ def export_records(records: List[PatentRecord], format: str, output_path: str):
128
+ format = format.lower()
129
+ try:
130
+ if format == "csv":
131
+ _export_csv(records, output_path)
132
+ elif format == "json":
133
+ _export_json(records, output_path)
134
+ elif format == "bibtex":
135
+ _export_bibtex(records, output_path)
136
+ elif format == "markdown":
137
+ _export_markdown(records, output_path)
138
+ elif format == "pdf":
139
+ _export_pdf(records, output_path)
140
+ else:
141
+ raise ValueError(f"Unsupported format: {format}")
142
+ except Exception as e:
143
+ print(f"ERR: Export to {format} failed. Reason: {e}")
144
+ raise
145
+
146
+ def export_pdf(collection: List[PatentRecord], path: str) -> None:
147
+ """
148
+ Export patent collection to PDF file.
149
+ Generates a title page followed by one page per patent.
150
+
151
+ Args:
152
+ collection: List of PatentRecord objects to export
153
+ path: Output file path (e.g., '/path/to/export.pdf')
154
+
155
+ Raises:
156
+ ValueError: If collection is empty
157
+ IOError: If file cannot be written
158
+ """
159
+ if not collection:
160
+ raise ValueError("Cannot export empty collection.")
161
+ try:
162
+ _export_pdf(collection, path)
163
+ except Exception as e:
164
+ print(f"ERR: PDF export failed. Reason: {e}")
165
+ raise