nber-cli 0.1.1__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nber-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A command-line tool to download NBER papers.
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -9,6 +9,7 @@ Requires-Dist: aiohttp-retry
9
9
  Requires-Dist: aiosqlite
10
10
  Requires-Dist: certifi
11
11
  Requires-Dist: fake-useragent
12
+ Requires-Dist: flask
12
13
  Requires-Dist: orjson
13
14
  Description-Content-Type: text/markdown
14
15
 
@@ -33,21 +34,39 @@ nber-cli --help
33
34
 
34
35
  - Download the certain paper to default directory:
35
36
  ```bash
36
- nber-cli paper_id
37
+ nber-cli --download paper_id [paper_id ...]
38
+ ```
39
+
40
+ - Shorthand:
41
+ ```bash
42
+ nber-cli -d paper_id [paper_id ...]
37
43
  ```
38
44
 
39
45
  - Download the certain paper to a specific directory:
40
46
  ```bash
41
- nber-cli paper_id --save_path /path/to/directory
47
+ nber-cli --download paper_id --save_path /path/to/directory
42
48
  ```
43
49
 
44
50
  An example of downloading a paper with ID `w1234` to the specific directory:
45
51
  ```bash
46
52
  (base) ~/Documents/Github/nber_cli git:[master]
47
- nber-cli w1234 --save_path ~/Downloads/nber-cli
53
+ nber-cli --download w1234 --save_path ~/Downloads/nber-cli
48
54
  2025-06-23 12:00:29,266 - INFO - Loaded 1 ok ids and 0 fail ids from db.
49
55
  2025-06-23 12:00:41,097 - INFO - Successfully downloaded w1234 to /Users/sepinetam/Downloads/nber-cli/w1234.pdf
56
+
50
57
  ```
51
58
 
59
+ You can also download multiple papers at once:
60
+ ```bash
61
+ nber-cli --download w1234 w5678
62
+ ```
63
+ ## Web UI
64
+
65
+ Start a simple web server with:
66
+ ```bash
67
+ nber-cli-web
68
+ ```
69
+
70
+
52
71
  ## LICENSE
53
72
  [APACHE-2.0](LICENSE)
@@ -19,21 +19,39 @@ nber-cli --help
19
19
 
20
20
  - Download the certain paper to default directory:
21
21
  ```bash
22
- nber-cli paper_id
22
+ nber-cli --download paper_id [paper_id ...]
23
+ ```
24
+
25
+ - Shorthand:
26
+ ```bash
27
+ nber-cli -d paper_id [paper_id ...]
23
28
  ```
24
29
 
25
30
  - Download the certain paper to a specific directory:
26
31
  ```bash
27
- nber-cli paper_id --save_path /path/to/directory
32
+ nber-cli --download paper_id --save_path /path/to/directory
28
33
  ```
29
34
 
30
35
  An example of downloading a paper with ID `w1234` to the specific directory:
31
36
  ```bash
32
37
  (base) ~/Documents/Github/nber_cli git:[master]
33
- nber-cli w1234 --save_path ~/Downloads/nber-cli
38
+ nber-cli --download w1234 --save_path ~/Downloads/nber-cli
34
39
  2025-06-23 12:00:29,266 - INFO - Loaded 1 ok ids and 0 fail ids from db.
35
40
  2025-06-23 12:00:41,097 - INFO - Successfully downloaded w1234 to /Users/sepinetam/Downloads/nber-cli/w1234.pdf
41
+
36
42
  ```
37
43
 
44
+ You can also download multiple papers at once:
45
+ ```bash
46
+ nber-cli --download w1234 w5678
47
+ ```
48
+ ## Web UI
49
+
50
+ Start a simple web server with:
51
+ ```bash
52
+ nber-cli-web
53
+ ```
54
+
55
+
38
56
  ## LICENSE
39
57
  [APACHE-2.0](LICENSE)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nber-cli"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "A command-line tool to download NBER papers."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -10,11 +10,13 @@ dependencies = [
10
10
  "orjson",
11
11
  "fake_useragent",
12
12
  "aiohttp-retry",
13
- "certifi"
13
+ "certifi",
14
+ "flask"
14
15
  ]
15
16
 
16
17
  [project.scripts]
17
18
  nber-cli = "nber_cli:main"
19
+ nber-cli-web = "nber_cli.webui:run"
18
20
 
19
21
  [build-system]
20
22
  requires = ["hatchling"]
@@ -0,0 +1,27 @@
1
+ import argparse
2
+ import asyncio
3
+ import os
4
+
5
+ from .core.download.downloader import main_download_multiple
6
+
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(description="Download NBER papers.")
10
+ parser.add_argument(
11
+ "-d",
12
+ "-D",
13
+ "--download",
14
+ dest="paper_ids",
15
+ required=True,
16
+ nargs='+',
17
+ type=str,
18
+ help="One or more NBER paper IDs (e.g., w1234 w5678).")
19
+ parser.add_argument(
20
+ "--save_path",
21
+ type=str,
22
+ default=os.path.expanduser("~/Documents/nber_paper"),
23
+ help="The directory to save the downloaded paper. Defaults to ~/Documents/nber_paper.")
24
+
25
+ args = parser.parse_args()
26
+
27
+ asyncio.run(main_download_multiple(args.paper_ids, args.save_path))
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) 2025 - Present Sepine Tam, Inc. All Rights Reserved
5
+ #
6
+ # @Author : Sepine Tam
7
+ # @Email : sepinetam@gmail.com
8
+ # @File : __init__.py.py
File without changes
@@ -7,21 +7,18 @@
7
7
  # @Email : sepinetam@gmail.com
8
8
  # @File : downloader.py
9
9
 
10
- import aiohttp
11
10
  import asyncio
12
- import aiosqlite
11
+ import logging
13
12
  import os
14
- import time
15
13
  import random
16
- import orjson # 比json更快的JSON库
17
- from fake_useragent import UserAgent
18
- from typing import List, Set, Dict, Any
19
- import logging
20
14
  import ssl
15
+ import time
16
+ from typing import List
17
+
18
+ import aiosqlite
21
19
  import certifi
22
- from aiohttp_retry import RetryClient, ExponentialRetry
23
- import functools
24
- import concurrent.futures
20
+ from aiohttp_retry import ExponentialRetry, RetryClient
21
+ from fake_useragent import UserAgent
25
22
 
26
23
  # 设置日志记录
27
24
  logging.basicConfig(
@@ -47,7 +44,8 @@ ID_CACHE = {
47
44
 
48
45
  # 创建数据库连接池
49
46
  db_pool = None
50
- DB_PATH = os.path.expanduser("~/.nber_cli_state.db") # 数据库路径,放在用户主目录下
47
+ DB_PATH = os.path.expanduser("~/.nber_cli_state.db") # 数据库路径,放在用户主目录下
48
+
51
49
 
52
50
  async def create_db_pool():
53
51
  """创建数据库连接池"""
@@ -55,16 +53,19 @@ async def create_db_pool():
55
53
  if db_pool is None:
56
54
  db_pool = [await aiosqlite.connect(DB_PATH) for _ in range(DB_POOL_SIZE)]
57
55
 
56
+
58
57
  async def close_db_pool():
59
58
  """关闭数据库连接池"""
60
59
  if db_pool:
61
60
  for conn in db_pool:
62
61
  await conn.close()
63
62
 
63
+
64
64
  async def get_db_conn():
65
65
  """从连接池中获取一个数据库连接"""
66
66
  return random.choice(db_pool)
67
67
 
68
+
68
69
  async def init_db():
69
70
  """初始化数据库"""
70
71
  conn = await get_db_conn()
@@ -78,7 +79,8 @@ async def init_db():
78
79
  )
79
80
  await conn.commit()
80
81
  finally:
81
- pass # 不关闭连接,因为它是从连接池中获取的
82
+ pass # 不关闭连接,因为它是从连接池中获取的
83
+
82
84
 
83
85
  async def load_ids_from_db():
84
86
  """从数据库加载已处理的ID到内存缓存"""
@@ -91,10 +93,12 @@ async def load_ids_from_db():
91
93
  else:
92
94
  ID_CACHE["fail_ids"].add(row[0])
93
95
  ID_CACHE["last_update"] = time.time()
94
- logger.info(f"Loaded {len(ID_CACHE['ok_ids'])} ok ids and {len(ID_CACHE['fail_ids'])} fail ids from db.")
96
+ logger.info(
97
+ f"Loaded {len(ID_CACHE['ok_ids'])} ok ids and {len(ID_CACHE['fail_ids'])} fail ids from db.")
95
98
  finally:
96
99
  pass
97
100
 
101
+
98
102
  async def get_paper_state(paper_id: str) -> str:
99
103
  """获取特定论文的下载状态"""
100
104
  if paper_id in ID_CACHE["ok_ids"]:
@@ -103,6 +107,7 @@ async def get_paper_state(paper_id: str) -> str:
103
107
  return "fail"
104
108
  return None
105
109
 
110
+
106
111
  async def update_paper_state(paper_id: str, status: str):
107
112
  """更新论文的下载状态"""
108
113
  conn = await get_db_conn()
@@ -123,18 +128,24 @@ async def update_paper_state(paper_id: str, status: str):
123
128
  finally:
124
129
  pass
125
130
 
131
+
126
132
  async def download_paper(paper_id: str, save_path: str):
127
133
  """下载单个NBER论文"""
128
- if await get_paper_state(paper_id) == 'ok':
129
- logger.info(f"Skipping {paper_id}, already downloaded.")
130
- return
131
-
132
- url = f"https://www.nber.org/papers/{paper_id}.pdf"
133
134
  filepath = os.path.join(save_path, f"{paper_id}.pdf")
134
135
 
135
136
  # 确保保存路径存在
136
137
  os.makedirs(save_path, exist_ok=True)
137
138
 
139
+ state = await get_paper_state(paper_id)
140
+ if state == 'ok' and os.path.exists(filepath):
141
+ logger.info(f"Skipping {paper_id}, already downloaded.")
142
+ return
143
+ if state == 'ok' and not os.path.exists(filepath):
144
+ logger.info(
145
+ f"{paper_id} marked as downloaded but file missing, re-downloading.")
146
+
147
+ url = f"https://www.nber.org/papers/{paper_id}.pdf"
148
+
138
149
  ua = UserAgent()
139
150
  headers = {'User-Agent': ua.random}
140
151
  retry_options = ExponentialRetry(attempts=MAX_RETRIES)
@@ -147,21 +158,27 @@ async def download_paper(paper_id: str, save_path: str):
147
158
  content = await response.read()
148
159
  with open(filepath, 'wb') as f:
149
160
  f.write(content)
150
- logger.info(f"Successfully downloaded {paper_id} to {filepath}")
161
+ logger.info(
162
+ f"Successfully downloaded {paper_id} to {filepath}")
151
163
  await update_paper_state(paper_id, 'ok')
152
164
  else:
153
- logger.error(f"Failed to download {paper_id}, status code: {response.status}")
165
+ logger.error(
166
+ f"Failed to download {paper_id}, status code: {response.status}")
154
167
  await update_paper_state(paper_id, 'fail')
155
168
  except Exception as e:
156
169
  logger.error(f"An error occurred while downloading {paper_id}: {e}")
157
170
  await update_paper_state(paper_id, 'fail')
158
171
 
159
- async def main_download(paper_id: str, save_path: str):
160
- """主下载函数,用于CLI调用"""
172
+
173
+ async def main_download_multiple(paper_ids: List[str], save_path: str):
174
+ """主下载函数,可下载多个paper"""
161
175
  await create_db_pool()
162
176
  await init_db()
163
177
  await load_ids_from_db()
164
- await download_paper(paper_id, save_path)
178
+ await asyncio.gather(*(download_paper(pid, save_path) for pid in paper_ids))
165
179
  await close_db_pool()
166
180
 
167
181
 
182
+ async def main_download(paper_id: str, save_path: str):
183
+ """向后兼容的单文件下载接口"""
184
+ await main_download_multiple([paper_id], save_path)
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+ import os
3
+
4
+ from flask import Flask, render_template_string, request
5
+
6
+ from ..core.download.downloader import main_download_multiple
7
+
8
+ DEFAULT_SAVE_PATH = os.path.expanduser("~/Documents/nber_paper")
9
+ app = Flask(__name__)
10
+
11
+ FORM = """
12
+ <!doctype html>
13
+ <html>
14
+ <head>
15
+ <title>NBER CLI Web</title>
16
+ <style>
17
+ body {font-family: Arial, sans-serif; margin: 2em;}
18
+ textarea {width: 100%; height: 6em;}
19
+ .message {color: green;}
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <h1>Download NBER Papers</h1>
24
+ <form method=post>
25
+ <label for="paper_ids">Paper IDs (space or newline separated):</label><br>
26
+ <textarea name="paper_ids" id="paper_ids"></textarea><br>
27
+ <input type=submit value=Download>
28
+ </form>
29
+ <p class="message">{{message}}</p>
30
+ </body>
31
+ </html>
32
+ """
33
+
34
+
35
+ @app.route('/', methods=['GET', 'POST'])
36
+ def index():
37
+ message = ''
38
+ if request.method == 'POST':
39
+ ids_raw = request.form.get('paper_ids', '')
40
+ paper_ids = [pid.strip() for pid in ids_raw.split() if pid.strip()]
41
+ if paper_ids:
42
+ asyncio.run(main_download_multiple(paper_ids, DEFAULT_SAVE_PATH))
43
+ message = f"Downloaded {', '.join(paper_ids)}"
44
+ else:
45
+ message = 'Please enter at least one paper ID.'
46
+ return render_template_string(FORM, message=message)
47
+
48
+
49
+ def run():
50
+ app.run()
@@ -124,6 +124,15 @@ wheels = [
124
124
  { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
125
125
  ]
126
126
 
127
+ [[package]]
128
+ name = "blinker"
129
+ version = "1.9.0"
130
+ source = { registry = "https://pypi.org/simple" }
131
+ sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
132
+ wheels = [
133
+ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
134
+ ]
135
+
127
136
  [[package]]
128
137
  name = "certifi"
129
138
  version = "2025.6.15"
@@ -133,6 +142,27 @@ wheels = [
133
142
  { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
134
143
  ]
135
144
 
145
+ [[package]]
146
+ name = "click"
147
+ version = "8.2.1"
148
+ source = { registry = "https://pypi.org/simple" }
149
+ dependencies = [
150
+ { name = "colorama", marker = "sys_platform == 'win32'" },
151
+ ]
152
+ sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
153
+ wheels = [
154
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
155
+ ]
156
+
157
+ [[package]]
158
+ name = "colorama"
159
+ version = "0.4.6"
160
+ source = { registry = "https://pypi.org/simple" }
161
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
162
+ wheels = [
163
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
164
+ ]
165
+
136
166
  [[package]]
137
167
  name = "fake-useragent"
138
168
  version = "2.2.0"
@@ -142,6 +172,23 @@ wheels = [
142
172
  { url = "https://files.pythonhosted.org/packages/51/37/b3ea9cd5558ff4cb51957caca2193981c6b0ff30bd0d2630ac62505d99d0/fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24", size = 161695, upload-time = "2025-04-14T15:32:17.732Z" },
143
173
  ]
144
174
 
175
+ [[package]]
176
+ name = "flask"
177
+ version = "3.1.1"
178
+ source = { registry = "https://pypi.org/simple" }
179
+ dependencies = [
180
+ { name = "blinker" },
181
+ { name = "click" },
182
+ { name = "itsdangerous" },
183
+ { name = "jinja2" },
184
+ { name = "markupsafe" },
185
+ { name = "werkzeug" },
186
+ ]
187
+ sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440, upload-time = "2025-05-13T15:01:17.447Z" }
188
+ wheels = [
189
+ { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305, upload-time = "2025-05-13T15:01:15.591Z" },
190
+ ]
191
+
145
192
  [[package]]
146
193
  name = "frozenlist"
147
194
  version = "1.7.0"
@@ -228,6 +275,75 @@ wheels = [
228
275
  { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
229
276
  ]
230
277
 
278
+ [[package]]
279
+ name = "itsdangerous"
280
+ version = "2.2.0"
281
+ source = { registry = "https://pypi.org/simple" }
282
+ sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
283
+ wheels = [
284
+ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
285
+ ]
286
+
287
+ [[package]]
288
+ name = "jinja2"
289
+ version = "3.1.6"
290
+ source = { registry = "https://pypi.org/simple" }
291
+ dependencies = [
292
+ { name = "markupsafe" },
293
+ ]
294
+ sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
295
+ wheels = [
296
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
297
+ ]
298
+
299
+ [[package]]
300
+ name = "markupsafe"
301
+ version = "3.0.2"
302
+ source = { registry = "https://pypi.org/simple" }
303
+ sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
304
+ wheels = [
305
+ { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
306
+ { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
307
+ { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
308
+ { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
309
+ { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
310
+ { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
311
+ { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
312
+ { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
313
+ { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
314
+ { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
315
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
316
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
317
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
318
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
319
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
320
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
321
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
322
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
323
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
324
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
325
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
326
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
327
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
328
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
329
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
330
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
331
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
332
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
333
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
334
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
335
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
336
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
337
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
338
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
339
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
340
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
341
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
342
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
343
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
344
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
345
+ ]
346
+
231
347
  [[package]]
232
348
  name = "multidict"
233
349
  version = "6.5.0"
@@ -311,7 +427,7 @@ wheels = [
311
427
 
312
428
  [[package]]
313
429
  name = "nber-cli"
314
- version = "0.1.1"
430
+ version = "0.1.3"
315
431
  source = { editable = "." }
316
432
  dependencies = [
317
433
  { name = "aiohttp" },
@@ -319,6 +435,7 @@ dependencies = [
319
435
  { name = "aiosqlite" },
320
436
  { name = "certifi" },
321
437
  { name = "fake-useragent" },
438
+ { name = "flask" },
322
439
  { name = "orjson" },
323
440
  ]
324
441
 
@@ -329,6 +446,7 @@ requires-dist = [
329
446
  { name = "aiosqlite" },
330
447
  { name = "certifi" },
331
448
  { name = "fake-useragent" },
449
+ { name = "flask" },
332
450
  { name = "orjson" },
333
451
  ]
334
452
 
@@ -467,6 +585,18 @@ wheels = [
467
585
  { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
468
586
  ]
469
587
 
588
+ [[package]]
589
+ name = "werkzeug"
590
+ version = "3.1.3"
591
+ source = { registry = "https://pypi.org/simple" }
592
+ dependencies = [
593
+ { name = "markupsafe" },
594
+ ]
595
+ sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
596
+ wheels = [
597
+ { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
598
+ ]
599
+
470
600
  [[package]]
471
601
  name = "yarl"
472
602
  version = "1.20.1"
@@ -1,16 +0,0 @@
1
- import argparse
2
- import asyncio
3
- import os
4
- from .downloader import main_download
5
-
6
- def main():
7
- parser = argparse.ArgumentParser(description="Download NBER papers.")
8
- parser.add_argument("paper_id", type=str, help="The NBER paper ID (e.g., w1234).")
9
- parser.add_argument("--save_path", type=str, default=os.path.expanduser("~/Documents/nber_paper"),
10
- help="The directory to save the downloaded paper. Defaults to ~/Documents/nber_paper.")
11
-
12
- args = parser.parse_args()
13
-
14
- asyncio.run(main_download(args.paper_id, args.save_path))
15
-
16
-
File without changes
File without changes
File without changes