instapaper-scraper 1.0.0__py3-none-any.whl → 1.1.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.
@@ -2,29 +2,17 @@ import os
2
2
  import json
3
3
  import sqlite3
4
4
  import logging
5
+ import csv
5
6
  from typing import List, Dict, Any
6
7
 
8
+ from .constants import INSTAPAPER_READ_URL, KEY_ID, KEY_TITLE, KEY_URL
9
+
7
10
  # Constants for file operations
8
11
  JSON_INDENT = 4
9
12
 
10
- # Constants for CSV output
11
- CSV_HEADER = "id,title,url\n"
12
- CSV_DELIMITER = ","
13
- CSV_ROW_FORMAT = "{id},{title},{url}\n"
14
-
15
13
  # Constants for SQLite output
16
14
  SQLITE_TABLE_NAME = "articles"
17
- SQLITE_ID_COL = "id"
18
- SQLITE_TITLE_COL = "title"
19
- SQLITE_URL_COL = "url"
20
- SQLITE_CREATE_TABLE_SQL = f"""
21
- CREATE TABLE IF NOT EXISTS {SQLITE_TABLE_NAME} (
22
- {SQLITE_ID_COL} TEXT PRIMARY KEY,
23
- {SQLITE_TITLE_COL} TEXT NOT NULL,
24
- {SQLITE_URL_COL} TEXT NOT NULL
25
- )
26
- """
27
- SQLITE_INSERT_SQL = f"INSERT OR REPLACE INTO {SQLITE_TABLE_NAME} ({SQLITE_ID_COL}, {SQLITE_TITLE_COL}, {SQLITE_URL_COL}) VALUES (:{SQLITE_ID_COL}, :{SQLITE_TITLE_COL}, :{SQLITE_URL_COL})"
15
+ SQLITE_INSTAPAPER_URL_COL = "instapaper_url"
28
16
 
29
17
  # Constants for logging messages
30
18
  LOG_NO_ARTICLES = "No articles found to save."
@@ -32,25 +20,56 @@ LOG_SAVED_ARTICLES = "Saved {count} articles to {filename}"
32
20
  LOG_UNKNOWN_FORMAT = "Unknown output format: {format}"
33
21
 
34
22
 
35
- def save_to_csv(data: List[Dict[str, Any]], filename: str):
23
+ def get_sqlite_create_table_sql(add_instapaper_url: bool = False) -> str:
24
+ """Returns the SQL statement to create the articles table."""
25
+ columns = [
26
+ f"{KEY_ID} TEXT PRIMARY KEY",
27
+ f"{KEY_TITLE} TEXT NOT NULL",
28
+ f"{KEY_URL} TEXT NOT NULL",
29
+ ]
30
+ if add_instapaper_url:
31
+ # The GENERATED ALWAYS AS syntax was added in SQLite 3.31.0
32
+ if sqlite3.sqlite_version_info >= (3, 31, 0):
33
+ columns.append(
34
+ f"{SQLITE_INSTAPAPER_URL_COL} TEXT GENERATED ALWAYS AS ('{INSTAPAPER_READ_URL}' || {KEY_ID}) VIRTUAL"
35
+ )
36
+ else:
37
+ columns.append(f"{SQLITE_INSTAPAPER_URL_COL} TEXT")
38
+
39
+ return f"CREATE TABLE IF NOT EXISTS {SQLITE_TABLE_NAME} ({', '.join(columns)})"
40
+
41
+
42
+ def get_sqlite_insert_sql(add_instapaper_url_manually: bool = False) -> str:
43
+ """Returns the SQL statement to insert an article."""
44
+ cols = [KEY_ID, KEY_TITLE, KEY_URL]
45
+ placeholders = [f":{KEY_ID}", f":{KEY_TITLE}", f":{KEY_URL}"]
46
+
47
+ if add_instapaper_url_manually:
48
+ cols.append(SQLITE_INSTAPAPER_URL_COL)
49
+ placeholders.append(f":{SQLITE_INSTAPAPER_URL_COL}")
50
+
51
+ return f"INSERT OR REPLACE INTO {SQLITE_TABLE_NAME} ({', '.join(cols)}) VALUES ({', '.join(placeholders)})"
52
+
53
+
54
+ def save_to_csv(
55
+ data: List[Dict[str, Any]], filename: str, add_instapaper_url: bool = False
56
+ ) -> None:
36
57
  """Saves a list of articles to a CSV file."""
37
58
  os.makedirs(os.path.dirname(filename), exist_ok=True)
38
59
  with open(filename, "w", newline="", encoding="utf-8") as f:
39
- f.write(CSV_HEADER)
40
- for article in data:
41
- # Basic CSV quoting for titles with commas
42
- title = article[SQLITE_TITLE_COL]
43
- if CSV_DELIMITER in title:
44
- title = f'"{title}"'
45
- f.write(
46
- CSV_ROW_FORMAT.format(
47
- id=article[SQLITE_ID_COL], title=title, url=article[SQLITE_URL_COL]
48
- )
49
- )
60
+ fieldnames = [KEY_ID, KEY_TITLE, KEY_URL]
61
+ if add_instapaper_url:
62
+ # Insert instapaper_url after the id column
63
+ fieldnames.insert(1, SQLITE_INSTAPAPER_URL_COL)
64
+
65
+ writer = csv.DictWriter(f, fieldnames=fieldnames, quoting=csv.QUOTE_ALL)
66
+ writer.writeheader()
67
+ writer.writerows(data)
68
+
50
69
  logging.info(LOG_SAVED_ARTICLES.format(count=len(data), filename=filename))
51
70
 
52
71
 
53
- def save_to_json(data: List[Dict[str, Any]], filename: str):
72
+ def save_to_json(data: List[Dict[str, Any]], filename: str) -> None:
54
73
  """Saves a list of articles to a JSON file."""
55
74
  os.makedirs(os.path.dirname(filename), exist_ok=True)
56
75
  with open(filename, "w", encoding="utf-8") as f:
@@ -58,19 +77,61 @@ def save_to_json(data: List[Dict[str, Any]], filename: str):
58
77
  logging.info(LOG_SAVED_ARTICLES.format(count=len(data), filename=filename))
59
78
 
60
79
 
61
- def save_to_sqlite(data: List[Dict[str, Any]], db_name: str):
80
+ def save_to_sqlite(
81
+ data: List[Dict[str, Any]], db_name: str, add_instapaper_url: bool = False
82
+ ) -> None:
62
83
  """Saves a list of articles to a SQLite database."""
63
84
  os.makedirs(os.path.dirname(db_name), exist_ok=True)
64
85
  conn = sqlite3.connect(db_name)
65
86
  cursor = conn.cursor()
66
- cursor.execute(SQLITE_CREATE_TABLE_SQL)
67
- cursor.executemany(SQLITE_INSERT_SQL, data)
87
+ cursor.execute(get_sqlite_create_table_sql(add_instapaper_url))
88
+
89
+ # For older SQLite versions, we need to manually add the URL
90
+ manual_insert_required = add_instapaper_url and sqlite3.sqlite_version_info < (
91
+ 3,
92
+ 31,
93
+ 0,
94
+ )
95
+ if manual_insert_required:
96
+ data_to_insert = [
97
+ {
98
+ **article,
99
+ SQLITE_INSTAPAPER_URL_COL: f"{INSTAPAPER_READ_URL}{article[KEY_ID]}",
100
+ }
101
+ for article in data
102
+ ]
103
+ else:
104
+ data_to_insert = data
105
+
106
+ insert_sql = get_sqlite_insert_sql(
107
+ add_instapaper_url_manually=manual_insert_required
108
+ )
109
+ cursor.executemany(insert_sql, data_to_insert)
110
+
68
111
  conn.commit()
69
112
  conn.close()
70
113
  logging.info(LOG_SAVED_ARTICLES.format(count=len(data), filename=db_name))
71
114
 
72
115
 
73
- def save_articles(data: List[Dict[str, Any]], format: str, filename: str):
116
+ def _correct_ext(filename: str, format: str) -> str:
117
+ """Corrects the filename extension based on the specified format."""
118
+ extension_map = {
119
+ "csv": ".csv",
120
+ "json": ".json",
121
+ "sqlite": ".db",
122
+ }
123
+ if format in extension_map:
124
+ name, _ = os.path.splitext(filename)
125
+ return name + extension_map[format]
126
+ return filename
127
+
128
+
129
+ def save_articles(
130
+ data: List[Dict[str, Any]],
131
+ format: str,
132
+ filename: str,
133
+ add_instapaper_url: bool = False,
134
+ ) -> None:
74
135
  """
75
136
  Dispatches to the correct save function based on the format.
76
137
  """
@@ -78,11 +139,23 @@ def save_articles(data: List[Dict[str, Any]], format: str, filename: str):
78
139
  logging.info(LOG_NO_ARTICLES)
79
140
  return
80
141
 
142
+ filename = _correct_ext(filename, format)
143
+
144
+ # Add the instapaper_url to the data for formats that don't auto-generate it
145
+ if add_instapaper_url and format in ("csv", "json"):
146
+ data = [
147
+ {
148
+ **article,
149
+ SQLITE_INSTAPAPER_URL_COL: f"{INSTAPAPER_READ_URL}{article[KEY_ID]}",
150
+ }
151
+ for article in data
152
+ ]
153
+
81
154
  if format == "csv":
82
- save_to_csv(data, filename=filename)
155
+ save_to_csv(data, filename=filename, add_instapaper_url=add_instapaper_url)
83
156
  elif format == "json":
84
157
  save_to_json(data, filename=filename)
85
158
  elif format == "sqlite":
86
- save_to_sqlite(data, db_name=filename)
159
+ save_to_sqlite(data, db_name=filename, add_instapaper_url=add_instapaper_url)
87
160
  else:
88
161
  logging.error(LOG_UNKNOWN_FORMAT.format(format=format))
@@ -0,0 +1,352 @@
1
+ Metadata-Version: 2.4
2
+ Name: instapaper-scraper
3
+ Version: 1.1.0
4
+ Summary: A tool to scrape articles from Instapaper.
5
+ Project-URL: Homepage, https://github.com/chriskyfung/InstapaperScraper
6
+ Project-URL: Source, https://github.com/chriskyfung/InstapaperScraper
7
+ Project-URL: Issues, https://github.com/chriskyfung/InstapaperScraper/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Classifier: Intended Audience :: End Users/Desktop
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
18
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
19
+ Classifier: Operating System :: OS Independent
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: beautifulsoup4~=4.14.2
24
+ Requires-Dist: certifi~=2025.11.12
25
+ Requires-Dist: charset-normalizer~=3.4.3
26
+ Requires-Dist: cryptography~=46.0.3
27
+ Requires-Dist: guara~=0.0.14
28
+ Requires-Dist: idna~=3.11
29
+ Requires-Dist: python-dotenv~=1.2.1
30
+ Requires-Dist: requests~=2.32.5
31
+ Requires-Dist: soupsieve~=2.8
32
+ Requires-Dist: typing_extensions~=4.15.0
33
+ Requires-Dist: urllib3<2.7,>=2.5
34
+ Requires-Dist: tomli~=2.0.1; python_version < "3.11"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest; extra == "dev"
37
+ Requires-Dist: pytest-cov; extra == "dev"
38
+ Requires-Dist: ruff; extra == "dev"
39
+ Requires-Dist: types-requests; extra == "dev"
40
+ Requires-Dist: types-beautifulsoup4; extra == "dev"
41
+ Requires-Dist: requests-mock; extra == "dev"
42
+ Requires-Dist: build; extra == "dev"
43
+ Requires-Dist: twine; extra == "dev"
44
+ Requires-Dist: mypy; extra == "dev"
45
+ Requires-Dist: pre-commit; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # Instapaper Scraper
49
+
50
+ <!-- Badges -->
51
+ <p align="center">
52
+ <a href="https://github.com/chriskyfung/InstapaperScraper">
53
+ <img src="https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fchriskyfung%2FInstapaperScraper%2Frefs%2Fheads%2Fmaster%2Fpyproject.toml" alt="Python Version from PEP 621 TOML">
54
+ </a>
55
+ <a href="https://github.com/astral-sh/ruff">
56
+ <img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fastral-sh%2Fruff%2Fmain%2Fassets%2Fbadge%2Fv2.json" alt="Ruff">
57
+ </a>
58
+ <a href="https://codecov.io/gh/chriskyfung/InstapaperScraper">
59
+ <img src="https://codecov.io/gh/chriskyfung/InstapaperScraper/graph/badge.svg" alt="Code Coverage">
60
+ </a>
61
+ <a href="https://github.com/chriskyfung/InstapaperScraper/actions/workflows/ci.yml">
62
+ <img src="https://github.com/chriskyfung/InstapaperScraper/actions/workflows/ci.yml/badge.svg" alt="CI Status">
63
+ </a>
64
+ <a href="https://pypi.org/project/instapaper-scraper/">
65
+ <img src="https://img.shields.io/pypi/v/instapaper-scraper.svg" alt="PyPI version">
66
+ </a>
67
+ <a href="https://pepy.tech/projects/instapaper-scraper">
68
+ <img src="https://static.pepy.tech/personalized-badge/instapaper-scraper?period=total&left_text=downloads" alt="PyPI Downloads">
69
+ </a>
70
+ <a href="https://www.gnu.org/licenses/gpl-3.0.en.html">
71
+ <img src="https://img.shields.io/github/license/chriskyfung/InstapaperScraper" alt="GitHub License">
72
+ </a>
73
+ <a href="https://github.com/sponsors/chriskyfung" title="Sponsor on GitHub">
74
+ <img src="https://img.shields.io/badge/Sponsor-GitHub-purple?logo=github" alt="GitHub Sponsors Default">
75
+ </a>
76
+ <a href="https://www.buymeacoffee.com/chriskyfung" title="Buy Me A Coffee">
77
+ <img src="https://img.shields.io/badge/Support%20Me-Coffee-ffdd00?logo=buy-me-a-coffee&logoColor=white" alt="Buy Me A Coffee">
78
+ </a>
79
+ </p>
80
+
81
+ A powerful and reliable Python tool to automate the export of all your saved Instapaper bookmarks into various formats, giving you full ownership of your data.
82
+
83
+ ## ✨ Features
84
+
85
+ - Scrapes all bookmarks from your Instapaper account.
86
+ - Supports scraping from specific folders.
87
+ - Exports data to CSV, JSON, or a SQLite database.
88
+ - Securely stores your session for future runs.
89
+ - Modern, modular, and tested architecture.
90
+
91
+ ## 🚀 Getting Started
92
+
93
+ ### 📋 1. Requirements
94
+
95
+ - Python 3.9+
96
+
97
+ ### 📦 2. Installation
98
+
99
+ This package is available on PyPI and can be installed with pip:
100
+
101
+ ```sh
102
+ pip install instapaper-scraper
103
+ ```
104
+
105
+ ### 💻 3. Usage
106
+
107
+ Run the tool from the command line, specifying your desired output format:
108
+
109
+ ```sh
110
+ # Scrape and export to the default CSV format
111
+ instapaper-scraper
112
+
113
+ # Scrape and export to JSON
114
+ instapaper-scraper --format json
115
+
116
+ # Scrape and export to a SQLite database with a custom name
117
+ instapaper-scraper --format sqlite --output my_articles.db
118
+ ```
119
+
120
+ ## ⚙️ Configuration
121
+
122
+ ### 🔐 Authentication
123
+
124
+ The script authenticates using one of the following methods, in order of priority:
125
+
126
+ 1. **Command-line Arguments**: Provide your username and password directly when running the script:
127
+
128
+ ```sh
129
+ instapaper-scraper --username your_username --password your_password
130
+ ```
131
+
132
+ 2. **Session Files (`.session_key`, `.instapaper_session`)**: The script attempts to load these files in the following order:
133
+ a. Path specified by `--session-file` or `--key-file` arguments.
134
+ b. Files in the current working directory (e.g., `./.session_key`).
135
+ c. Files in the user's configuration directory (`~/.config/instapaper-scraper/`).
136
+ After the first successful login, the script creates an encrypted `.instapaper_session` file and a `.session_key` file to reuse your session securely.
137
+
138
+ 3. **Interactive Prompt**: If no other method is available, the script will prompt you for your username and password.
139
+
140
+ > **Note on Security:** Your session file (`.instapaper_session`) and the encryption key (`.session_key`) are stored with secure permissions (read/write for the owner only) to protect your credentials.
141
+
142
+ ### 📁 Folder Configuration
143
+
144
+ You can define and quickly access your Instapaper folders using a `config.toml` file. The scraper will look for this file in the following locations (in order of precedence):
145
+
146
+ 1. The path specified by the `--config-path` argument.
147
+ 2. `config.toml` in the current working directory.
148
+ 3. `~/.config/instapaper-scraper/config.toml`
149
+
150
+ Here is an example of `config.toml`:
151
+
152
+ ```toml
153
+ # Default output filename for non-folder mode
154
+ output_filename = "home-articles.csv"
155
+
156
+ [[folders]]
157
+ key = "ml"
158
+ id = "1234567"
159
+ slug = "machine-learning"
160
+ output_filename = "ml-articles.json"
161
+
162
+ [[folders]]
163
+ key = "python"
164
+ id = "7654321"
165
+ slug = "python-programming"
166
+ output_filename = "python-articles.db"
167
+ ```
168
+
169
+ - **output_filename (top-level)**: The default output filename to use when not in folder mode.
170
+ - **key**: A short alias for the folder.
171
+ - **id**: The folder ID from the Instapaper URL.
172
+ - **slug**: The human-readable part of the folder URL.
173
+ - **output_filename (folder-specific)**: A preset output filename for scraped articles from this specific folder.
174
+
175
+ When a `config.toml` file is present and no `--folder` argument is provided, the scraper will prompt you to select a folder. You can also specify a folder directly using the `--folder` argument with its key, ID, or slug. Use `--folder=none` to explicitly disable folder mode and scrape all articles.
176
+
177
+ ### 💻 Command-line Arguments
178
+
179
+ | Argument | Description |
180
+ | --- | --- |
181
+ | `--config-path <path>`| Path to the configuration file. Searches `~/.config/instapaper-scraper/config.toml` and `config.toml` in the current directory by default. |
182
+ | `--folder <value>` | Specify a folder by key, ID, or slug from your `config.toml`. **Requires a configuration file to be loaded.** Use `none` to explicitly disable folder mode. If a configuration file is not found or fails to load, and this option is used (not set to `none`), the program will exit. |
183
+ | `--format <format>` | Output format (`csv`, `json`, `sqlite`). Default: `csv`. |
184
+ | `--output <filename>` | Specify a custom output filename. The file extension will be automatically corrected to match the selected format. |
185
+ | `--username <user>` | Your Instapaper account username. |
186
+ | `--password <pass>` | Your Instapaper account password. |
187
+ | `--add-instapaper-url` | Adds a `instapaper_url` column to the output, containing a full, clickable URL for each article. |
188
+
189
+ ### 📄 Output Formats
190
+
191
+ You can control the output format using the `--format` argument. The supported formats are:
192
+
193
+ - `csv` (default): Exports data to `output/bookmarks.csv`.
194
+ - `json`: Exports data to `output/bookmarks.json`.
195
+ - `sqlite`: Exports data to an `articles` table in `output/bookmarks.db`.
196
+
197
+ If the `--format` flag is omitted, the script will default to `csv`.
198
+
199
+ When using `--output <filename>`, the file extension is automatically corrected to match the chosen format. For example, `instapaper-scraper --format json --output my_articles.txt` will create `my_articles.json`.
200
+
201
+ #### 📖 Opening Articles in Instapaper
202
+
203
+ The output data includes a unique `id` for each article. You can use this ID to construct a URL to the article's reader view: `https://www.instapaper.com/read/<article_id>`.
204
+
205
+ For convenience, you can use the `--add-instapaper-url` flag to have the script include a full, clickable URL in the output.
206
+
207
+ ```sh
208
+ instapaper-scraper --add-instapaper-url
209
+ ```
210
+
211
+ This adds a `instapaper_url` field to each article in the JSON output and a `instapaper_url` column in the CSV and SQLite outputs. The original `id` field is preserved.
212
+
213
+ ## 🛠️ How It Works
214
+
215
+ The tool is designed with a modular architecture for reliability and maintainability.
216
+
217
+ 1. **Authentication**: The `InstapaperAuthenticator` handles secure login and session management.
218
+ 2. **Scraping**: The `InstapaperClient` iterates through all pages of your bookmarks, fetching the metadata for each article with robust error handling and retries. Shared constants, like the Instapaper base URL, are managed through `src/instapaper_scraper/constants.py`.
219
+ 3. **Data Collection**: All fetched articles are aggregated into a single list.
220
+ 4. **Export**: Finally, the collected data is written to a file in your chosen format (`.csv`, `.json`, or `.db`).
221
+
222
+ ## 📊 Example Output
223
+
224
+ ### 📄 CSV (`output/bookmarks.csv`) (with --add-instapaper-url)
225
+
226
+ ```csv
227
+ "id","instapaper_url","title","url"
228
+ "999901234","https://www.instapaper.com/read/999901234","Article 1","https://www.example.com/page-1/"
229
+ "999002345","https://www.instapaper.com/read/999002345","Article 2","https://www.example.com/page-2/"
230
+ ```
231
+
232
+ ### 📄 JSON (`output/bookmarks.json`) (with --add-instapaper-url)
233
+
234
+ ```json
235
+ [
236
+ {
237
+ "id": "999901234",
238
+ "title": "Article 1",
239
+ "url": "https://www.example.com/page-1/",
240
+ "instapaper_url": "https://www.instapaper.com/read/999901234"
241
+ },
242
+ {
243
+ "id": "999002345",
244
+ "title": "Article 2",
245
+ "url": "https://www.example.com/page-2/",
246
+ "instapaper_url": "https://www.instapaper.com/read/999002345"
247
+ }
248
+ ]
249
+ ```
250
+
251
+ ### 🗄️ SQLite (`output/bookmarks.db`)
252
+
253
+ A SQLite database file is created with an `articles` table. The table includes `id`, `title`, and `url` columns. If the `--add-instapaper-url` flag is used, a `instapaper_url` column is also included. This feature is fully backward-compatible and will automatically adapt to the user's installed SQLite version, using an efficient generated column on modern versions (3.31.0+) and a fallback for older versions.
254
+
255
+ ## 🤗 Support and Community
256
+
257
+ - **🐛 Bug Reports:** For any bugs or unexpected behavior, please [open an issue on GitHub](https://github.com/chriskyfung/InstapaperScraper/issues).
258
+ - **💬 Questions & General Discussion:** For questions, feature requests, or general discussion, please use our [GitHub Discussions](https://github.com/chriskyfung/InstapaperScraper/discussions).
259
+
260
+ ## 🙏 Support the Project
261
+
262
+ `Instapaper Scraper` is a free and open-source project that requires significant time and effort to maintain and improve. If you find this tool useful, please consider supporting its development. Your contribution helps ensure the project stays healthy, active, and continuously updated.
263
+
264
+ - **[Sponsor on GitHub](https://github.com/sponsors/chriskyfung):** The best way to support the project with recurring monthly donations. Tiers with special rewards like priority support are available!
265
+ - **[Buy Me a Coffee](https://www.buymeacoffee.com/chriskyfung):** Perfect for a one-time thank you.
266
+
267
+ ## 🤝 Contributing
268
+
269
+ Contributions are welcome! Whether it's a bug fix, a new feature, or documentation improvements, please feel free to open a pull request.
270
+
271
+ Please read the **[Contribution Guidelines](CONTRIBUTING.md)** before you start.
272
+
273
+ ## 🧑‍💻 Development & Testing
274
+
275
+ This project uses `pytest` for testing, `ruff` for code formatting and linting, and `mypy` for static type checking.
276
+
277
+ ### 🔧 Setup
278
+
279
+ To install the development dependencies:
280
+
281
+ ```sh
282
+ pip install -e .[dev]
283
+ ```
284
+
285
+ To set up the pre-commit hooks:
286
+
287
+ ```sh
288
+ pre-commit install
289
+ ```
290
+
291
+ ### ▶️ Running the Scraper
292
+
293
+ To run the scraper directly without installing the package:
294
+
295
+ ```sh
296
+ python -m src.instapaper_scraper.cli
297
+ ```
298
+
299
+ ### ✅ Testing
300
+
301
+ To run the tests, execute the following command from the project root:
302
+
303
+ ```sh
304
+ pytest
305
+ ```
306
+
307
+ To check test coverage:
308
+
309
+ ```sh
310
+ pytest --cov=src/instapaper_scraper --cov-report=term-missing
311
+ ```
312
+
313
+ ### ✨ Code Quality
314
+
315
+ To format the code with `ruff`:
316
+
317
+ ```sh
318
+ ruff format .
319
+ ```
320
+
321
+ To check for linting errors with `ruff`:
322
+
323
+ ```sh
324
+ ruff check .
325
+ ```
326
+
327
+ To automatically fix linting errors:
328
+
329
+ ```sh
330
+ ruff check . --fix
331
+ ```
332
+
333
+ To run static type checking with `mypy`:
334
+
335
+ ```sh
336
+ mypy src
337
+ ```
338
+
339
+ ## 📜 Disclaimer
340
+
341
+ This script requires valid Instapaper credentials. Use it responsibly and in accordance with Instapaper’s Terms of Service.
342
+
343
+ ## 📄 License
344
+
345
+ This project is licensed under the terms of the **GNU General Public License v3.0**. See the [LICENSE](LICENSE) file for the full license text.
346
+
347
+ ## 🙏 Support the Project
348
+
349
+ `Instapaper Scraper` is a free and open-source project that requires significant time and effort to maintain and improve. If you find this tool useful, please consider supporting its development. Your contribution helps ensure the project stays healthy, active, and continuously updated.
350
+
351
+ - **[Sponsor on GitHub](https://github.com/sponsors/chriskyfung):** The best way to support the project with recurring monthly donations. Tiers with special rewards like priority support are available!
352
+ - **[Buy Me a Coffee](https://www.buymeacoffee.com/chriskyfung):** Perfect for a one-time thank you.
@@ -0,0 +1,13 @@
1
+ instapaper_scraper/__init__.py,sha256=qdcT3tp4KLufWH1u6tOuPVUQaXwakQD0gdjkwY4ljfg,206
2
+ instapaper_scraper/api.py,sha256=67ZeiVjsZpGspB8S3ni8FS6LBAOHXBc_oz3vEDWDNms,12672
3
+ instapaper_scraper/auth.py,sha256=OpgjbdI697FitumiyznWjey5-R2ZuxAEATaMz9NNnTc,7092
4
+ instapaper_scraper/cli.py,sha256=YL9c7kksmj5iGKRvVqG0KO4rBbhTg5c9Lgvsf_brRPA,7579
5
+ instapaper_scraper/constants.py,sha256=ubFWa47985lIz58qokMC0xQzTmCB6NOa17KFgWLn65E,403
6
+ instapaper_scraper/exceptions.py,sha256=CptHoZe4NOhdjOoyXkZEMFgQC6oKtzjRljywwDEtsTg,134
7
+ instapaper_scraper/output.py,sha256=DdwVNZ6dVK95rEGjIO0vR6h34sg6GGJjEe6ZFZc0LtE,5370
8
+ instapaper_scraper-1.1.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
9
+ instapaper_scraper-1.1.0.dist-info/METADATA,sha256=joCv87uWUarw_1Re2_2WxNm1ypPnMXqSVY1lYdmpNzI,14554
10
+ instapaper_scraper-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ instapaper_scraper-1.1.0.dist-info/entry_points.txt,sha256=7AvRgN5fvtas_Duxdz-JPbDN6A1Lq2GaTfTSv54afxA,67
12
+ instapaper_scraper-1.1.0.dist-info/top_level.txt,sha256=kiU9nLkqPOVPLsP4QMHuBFjAmoIKfftYmGV05daLrcc,19
13
+ instapaper_scraper-1.1.0.dist-info/RECORD,,