kiosque 0.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.
- kiosque/__init__.py +159 -0
- kiosque/__main__.py +17 -0
- kiosque/api/__init__.py +0 -0
- kiosque/api/github.py +236 -0
- kiosque/api/pocket.py +133 -0
- kiosque/api/raindrop.py +208 -0
- kiosque/core/__init__.py +0 -0
- kiosque/core/client.py +137 -0
- kiosque/core/config.py +246 -0
- kiosque/core/website.py +473 -0
- kiosque/tui/__init__.py +0 -0
- kiosque/tui/github.py +129 -0
- kiosque/tui/kiosque.tcss +117 -0
- kiosque/tui/raindrop.py +233 -0
- kiosque/tui/tui.py +622 -0
- kiosque/website/01net.py +10 -0
- kiosque/website/__init__.py +0 -0
- kiosque/website/asahi.py +24 -0
- kiosque/website/aviationweek.py +149 -0
- kiosque/website/courrierinternational.py +99 -0
- kiosque/website/franceculture.py +26 -0
- kiosque/website/francetvinfo.py +19 -0
- kiosque/website/ft.py +9 -0
- kiosque/website/ladepeche.py +22 -0
- kiosque/website/latimes.py +24 -0
- kiosque/website/lefigaro.py +20 -0
- kiosque/website/lemonde.py +105 -0
- kiosque/website/lesechos.py +74 -0
- kiosque/website/letemps.py +21 -0
- kiosque/website/mediapart.py +38 -0
- kiosque/website/mondediplomatique.py +93 -0
- kiosque/website/nationalgeographic.py +30 -0
- kiosque/website/nbcnews.py +24 -0
- kiosque/website/newyorker.py +33 -0
- kiosque/website/nikkei.py +19 -0
- kiosque/website/nytimes.py +167 -0
- kiosque/website/policito.py +56 -0
- kiosque/website/pourlascience.py +74 -0
- kiosque/website/quantamagazine.py +20 -0
- kiosque/website/reporterre.py +39 -0
- kiosque/website/rugbyrama.py +31 -0
- kiosque/website/telerama.py +10 -0
- kiosque/website/theatlantic.py +18 -0
- kiosque/website/theconversation.py +15 -0
- kiosque/website/theguardian.py +15 -0
- kiosque/website/usinenouvelle.py +145 -0
- kiosque/website/washingtonpost.py +30 -0
- kiosque/website/yomiuri.py +12 -0
- kiosque-0.1.0.dist-info/METADATA +173 -0
- kiosque-0.1.0.dist-info/RECORD +52 -0
- kiosque-0.1.0.dist-info/WHEEL +4 -0
- kiosque-0.1.0.dist-info/entry_points.txt +2 -0
kiosque/__init__.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# from .aviationweek import AviationWeek
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .core.config import config_dict, configuration_file # noqa: F401
|
|
10
|
+
from .core.website import Website
|
|
11
|
+
from .tui.tui import main as tui_main
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def list_websites() -> None:
|
|
15
|
+
"""List all supported websites with their authentication status."""
|
|
16
|
+
website_dir = Path(__file__).parent / "website"
|
|
17
|
+
websites_info = []
|
|
18
|
+
|
|
19
|
+
for module_file in sorted(website_dir.glob("*.py")):
|
|
20
|
+
if module_file.stem.startswith("_"):
|
|
21
|
+
continue
|
|
22
|
+
|
|
23
|
+
content = module_file.read_text()
|
|
24
|
+
|
|
25
|
+
# Extract base_url
|
|
26
|
+
base_url = None
|
|
27
|
+
for line in content.split("\n"):
|
|
28
|
+
if "base_url = " in line and "http" in line:
|
|
29
|
+
# Extract URL from string
|
|
30
|
+
base_url = (
|
|
31
|
+
line.split('"')[1] if '"' in line else line.split("'")[1]
|
|
32
|
+
)
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
# Check if login_url is defined (indicates auth required)
|
|
36
|
+
needs_auth = "login_url = " in content
|
|
37
|
+
|
|
38
|
+
# Extract class name for display
|
|
39
|
+
class_name = None
|
|
40
|
+
for line in content.split("\n"):
|
|
41
|
+
if line.startswith("class ") and "(Website)" in line:
|
|
42
|
+
class_name = line.split("class ")[1].split("(")[0]
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
if base_url and class_name:
|
|
46
|
+
websites_info.append(
|
|
47
|
+
{
|
|
48
|
+
"name": class_name,
|
|
49
|
+
"url": base_url,
|
|
50
|
+
"auth": "Yes" if needs_auth else "No",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Print header
|
|
55
|
+
click.echo("\nSupported Websites:")
|
|
56
|
+
click.echo("=" * 80)
|
|
57
|
+
click.echo(f"{'Website':<30} {'Authentication':<15} {'URL':<35}")
|
|
58
|
+
click.echo("-" * 80)
|
|
59
|
+
|
|
60
|
+
# Print websites
|
|
61
|
+
for info in websites_info:
|
|
62
|
+
click.echo(f"{info['name']:<30} {info['auth']:<15} {info['url']:<35}")
|
|
63
|
+
|
|
64
|
+
click.echo("=" * 80)
|
|
65
|
+
click.echo(f"\nTotal: {len(websites_info)} websites supported\n")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@click.command(
|
|
69
|
+
help="Read newspaper articles in textual format. Launches TUI by default."
|
|
70
|
+
)
|
|
71
|
+
@click.argument("url_or_alias", required=False)
|
|
72
|
+
@click.argument("output", type=click.File("w"), required=False, default=None)
|
|
73
|
+
@click.option("-v", "--verbose", count=True, help="Verbosity level")
|
|
74
|
+
@click.option(
|
|
75
|
+
"--list-websites",
|
|
76
|
+
"show_list",
|
|
77
|
+
is_flag=True,
|
|
78
|
+
help="List all supported websites",
|
|
79
|
+
)
|
|
80
|
+
def main(
|
|
81
|
+
url_or_alias: str | None,
|
|
82
|
+
output: Path | None,
|
|
83
|
+
verbose: int,
|
|
84
|
+
show_list: bool,
|
|
85
|
+
) -> None:
|
|
86
|
+
# Handle --list-websites flag
|
|
87
|
+
if show_list:
|
|
88
|
+
list_websites()
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
# Launch TUI by default when no arguments provided
|
|
92
|
+
if url_or_alias is None:
|
|
93
|
+
tui_main()
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# Configure logging
|
|
97
|
+
logging.basicConfig(
|
|
98
|
+
level=logging.WARNING,
|
|
99
|
+
format="%(levelname)s: %(message)s",
|
|
100
|
+
)
|
|
101
|
+
logger = logging.getLogger()
|
|
102
|
+
|
|
103
|
+
if verbose == 1:
|
|
104
|
+
logger.setLevel(logging.INFO)
|
|
105
|
+
elif verbose > 1:
|
|
106
|
+
logger.setLevel(logging.DEBUG)
|
|
107
|
+
|
|
108
|
+
if isinstance(output, str):
|
|
109
|
+
output = Path(output)
|
|
110
|
+
|
|
111
|
+
library: dict[str, type[Website]] = dict()
|
|
112
|
+
|
|
113
|
+
for key, value in config_dict.items():
|
|
114
|
+
if not key.startswith("http"):
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
logging.debug(f"Parsing configuration for {key}")
|
|
118
|
+
if not key.endswith("/"):
|
|
119
|
+
key += "/"
|
|
120
|
+
|
|
121
|
+
library[key] = website = Website.instance(key).__class__
|
|
122
|
+
|
|
123
|
+
if "alias" in value:
|
|
124
|
+
for alias in value["alias"].split(","):
|
|
125
|
+
logging.debug(f"Setting alias '{alias}' for {website}")
|
|
126
|
+
website.alias.append(alias)
|
|
127
|
+
|
|
128
|
+
for alias in website.alias:
|
|
129
|
+
library[alias] = website
|
|
130
|
+
|
|
131
|
+
logging.debug(f"List of aliases: {library}")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
if url_or_alias == "tui":
|
|
135
|
+
tui_main()
|
|
136
|
+
elif url_or_alias in library:
|
|
137
|
+
library[url_or_alias]().save_latest_issue()
|
|
138
|
+
elif output is None or isinstance(output, Path):
|
|
139
|
+
instance = Website.instance(url_or_alias)
|
|
140
|
+
instance.write_text(url_or_alias, output)
|
|
141
|
+
else:
|
|
142
|
+
# If it should be written to -
|
|
143
|
+
instance = Website.instance(url_or_alias)
|
|
144
|
+
content = instance.full_text(url_or_alias)
|
|
145
|
+
output.write(content)
|
|
146
|
+
except ValueError as e:
|
|
147
|
+
logging.error(str(e))
|
|
148
|
+
raise click.ClickException(str(e))
|
|
149
|
+
except NotImplementedError as e:
|
|
150
|
+
logging.error(str(e))
|
|
151
|
+
raise click.ClickException(str(e))
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logging.error(f"An unexpected error occurred: {e}")
|
|
154
|
+
if verbose > 0:
|
|
155
|
+
raise
|
|
156
|
+
else:
|
|
157
|
+
raise click.ClickException(
|
|
158
|
+
f"An error occurred. Use -v for more details: {e}"
|
|
159
|
+
)
|
kiosque/__main__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# If we are running from a wheel, add the wheel to sys.path
|
|
5
|
+
# This allows the usage python pip-*.whl/pip install pip-*.whl
|
|
6
|
+
if __package__ == "":
|
|
7
|
+
# __file__ is kiosque-*.whl/kiosque/__main__.py
|
|
8
|
+
# first dirname call strips of '/__main__.py', second strips off '/pip'
|
|
9
|
+
# Resulting path is the name of the wheel itself
|
|
10
|
+
# Add that to sys.path so we can import pip
|
|
11
|
+
path = os.path.dirname(os.path.dirname(__file__))
|
|
12
|
+
sys.path.insert(0, path)
|
|
13
|
+
|
|
14
|
+
from kiosque import main
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
main()
|
kiosque/api/__init__.py
ADDED
|
File without changes
|
kiosque/api/github.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
from pydantic import BaseModel, Field, HttpUrl, field_validator
|
|
6
|
+
|
|
7
|
+
from ..core.config import config_dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GitHubRepo(BaseModel):
|
|
11
|
+
"""Model representing a GitHub repository."""
|
|
12
|
+
|
|
13
|
+
id: int
|
|
14
|
+
name: str
|
|
15
|
+
full_name: str
|
|
16
|
+
html_url: HttpUrl
|
|
17
|
+
description: str | None = None
|
|
18
|
+
language: str | None = None
|
|
19
|
+
stargazers_count: int = 0
|
|
20
|
+
topics: list[str] = Field(default_factory=list)
|
|
21
|
+
created_at: datetime
|
|
22
|
+
updated_at: datetime
|
|
23
|
+
pushed_at: datetime
|
|
24
|
+
homepage: HttpUrl | None = None
|
|
25
|
+
owner: dict[str, Any] # Contains login, avatar_url, etc.
|
|
26
|
+
|
|
27
|
+
# Additional fields for starred repos
|
|
28
|
+
starred_at: datetime | None = None
|
|
29
|
+
|
|
30
|
+
@field_validator("homepage", mode="before")
|
|
31
|
+
@classmethod
|
|
32
|
+
def validate_homepage(cls, v: str | None) -> str | None:
|
|
33
|
+
"""Validate homepage URL, converting empty strings to None."""
|
|
34
|
+
if v == "" or v is None:
|
|
35
|
+
return None
|
|
36
|
+
return v
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GitHubAPI:
|
|
40
|
+
"""API client for interacting with GitHub.
|
|
41
|
+
|
|
42
|
+
Go to https://github.com/settings/tokens
|
|
43
|
+
Create a classic token with 'public_repo' or 'repo' scope
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
github_config = config_dict.get("github", None)
|
|
48
|
+
assert github_config is not None, "GitHub configuration not found."
|
|
49
|
+
|
|
50
|
+
self.token = github_config.get("token")
|
|
51
|
+
self.headers = {
|
|
52
|
+
"Authorization": f"Bearer {self.token}",
|
|
53
|
+
"Accept": "application/vnd.github+json",
|
|
54
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
55
|
+
}
|
|
56
|
+
self.client = httpx.Client(headers=self.headers, timeout=30.0)
|
|
57
|
+
self.async_client = httpx.AsyncClient(
|
|
58
|
+
headers=self.headers, timeout=30.0
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def get_user_async(self) -> dict[str, Any]:
|
|
62
|
+
"""Get the authenticated user's information."""
|
|
63
|
+
response = await self.async_client.get("https://api.github.com/user")
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
return response.json()
|
|
66
|
+
|
|
67
|
+
async def get_starred_repos_async(
|
|
68
|
+
self, page: int = 1, per_page: int = 100
|
|
69
|
+
) -> list[GitHubRepo]:
|
|
70
|
+
"""Get starred repositories for the authenticated user.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
page: Page number (1-indexed)
|
|
74
|
+
per_page: Number of results per page (max 100)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of GitHubRepo objects
|
|
78
|
+
"""
|
|
79
|
+
# Use the star timestamp header to get when repos were starred
|
|
80
|
+
headers = {
|
|
81
|
+
**self.headers,
|
|
82
|
+
"Accept": "application/vnd.github.star+json",
|
|
83
|
+
}
|
|
84
|
+
response = await self.async_client.get(
|
|
85
|
+
"https://api.github.com/user/starred",
|
|
86
|
+
params={"page": page, "per_page": per_page},
|
|
87
|
+
headers=headers,
|
|
88
|
+
)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
|
|
91
|
+
repos = []
|
|
92
|
+
for item in response.json():
|
|
93
|
+
repo_data = item["repo"]
|
|
94
|
+
repo_data["starred_at"] = item["starred_at"]
|
|
95
|
+
repos.append(GitHubRepo(**repo_data))
|
|
96
|
+
|
|
97
|
+
return repos
|
|
98
|
+
|
|
99
|
+
async def get_all_starred_repos_async(self) -> list[GitHubRepo]:
|
|
100
|
+
"""Get all starred repositories (handles pagination automatically)."""
|
|
101
|
+
all_repos = []
|
|
102
|
+
page = 1
|
|
103
|
+
|
|
104
|
+
while True:
|
|
105
|
+
repos = await self.get_starred_repos_async(page=page, per_page=100)
|
|
106
|
+
if not repos:
|
|
107
|
+
break
|
|
108
|
+
all_repos.extend(repos)
|
|
109
|
+
if len(repos) < 100: # Last page
|
|
110
|
+
break
|
|
111
|
+
page += 1
|
|
112
|
+
|
|
113
|
+
return all_repos
|
|
114
|
+
|
|
115
|
+
async def get_readme_async(self, owner: str, repo: str) -> str | None:
|
|
116
|
+
"""Get the README content for a repository.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
owner: Repository owner (username or organization)
|
|
120
|
+
repo: Repository name
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
README content in markdown format, or None if not found
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
response = await self.async_client.get(
|
|
127
|
+
f"https://api.github.com/repos/{owner}/{repo}/readme",
|
|
128
|
+
headers={
|
|
129
|
+
**self.headers,
|
|
130
|
+
"Accept": "application/vnd.github.raw+json",
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
response.raise_for_status()
|
|
134
|
+
return response.text
|
|
135
|
+
except httpx.HTTPStatusError as e:
|
|
136
|
+
if e.response.status_code == 404:
|
|
137
|
+
return None # No README found
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
async def get_languages_async(
|
|
141
|
+
self, owner: str, repo: str
|
|
142
|
+
) -> dict[str, int]:
|
|
143
|
+
"""Get languages used in a repository.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
owner: Repository owner
|
|
147
|
+
repo: Repository name
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dictionary mapping language names to bytes of code
|
|
151
|
+
"""
|
|
152
|
+
response = await self.async_client.get(
|
|
153
|
+
f"https://api.github.com/repos/{owner}/{repo}/languages"
|
|
154
|
+
)
|
|
155
|
+
response.raise_for_status()
|
|
156
|
+
return response.json()
|
|
157
|
+
|
|
158
|
+
async def unstar_repo_async(self, owner: str, repo: str) -> None:
|
|
159
|
+
"""Unstar a repository.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
owner: Repository owner
|
|
163
|
+
repo: Repository name
|
|
164
|
+
"""
|
|
165
|
+
response = await self.async_client.delete(
|
|
166
|
+
f"https://api.github.com/user/starred/{owner}/{repo}"
|
|
167
|
+
)
|
|
168
|
+
response.raise_for_status()
|
|
169
|
+
|
|
170
|
+
async def star_repo_async(self, owner: str, repo: str) -> None:
|
|
171
|
+
"""Star a repository.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
owner: Repository owner
|
|
175
|
+
repo: Repository name
|
|
176
|
+
"""
|
|
177
|
+
response = await self.async_client.put(
|
|
178
|
+
f"https://api.github.com/user/starred/{owner}/{repo}"
|
|
179
|
+
)
|
|
180
|
+
response.raise_for_status()
|
|
181
|
+
|
|
182
|
+
async def is_starred_async(self, owner: str, repo: str) -> bool:
|
|
183
|
+
"""Check if a repository is starred.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
owner: Repository owner
|
|
187
|
+
repo: Repository name
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if starred, False otherwise
|
|
191
|
+
"""
|
|
192
|
+
response = await self.async_client.get(
|
|
193
|
+
f"https://api.github.com/user/starred/{owner}/{repo}"
|
|
194
|
+
)
|
|
195
|
+
return response.status_code == 204
|
|
196
|
+
|
|
197
|
+
async def async_retrieve(self, offset: int = 0) -> dict[str, Any]:
|
|
198
|
+
"""Legacy method for compatibility with TUI - returns formatted data.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
offset: Offset for pagination (not used, GitHub API uses pages)
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Dict with 'list' (dict of repos by ID) and 'count'
|
|
205
|
+
"""
|
|
206
|
+
repos = await self.get_all_starred_repos_async()
|
|
207
|
+
|
|
208
|
+
# Convert to format expected by TUI
|
|
209
|
+
items_dict = {str(repo.id): repo for repo in repos}
|
|
210
|
+
return {"list": items_dict, "count": len(repos)}
|
|
211
|
+
|
|
212
|
+
async def async_action(
|
|
213
|
+
self, action: str, repo_id: int, owner: str, repo: str
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Perform an action on a repository.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
action: Action to perform ('unstar', etc.)
|
|
219
|
+
repo_id: Repository ID (for compatibility)
|
|
220
|
+
owner: Repository owner
|
|
221
|
+
repo: Repository name
|
|
222
|
+
"""
|
|
223
|
+
if action == "unstar":
|
|
224
|
+
await self.unstar_repo_async(owner, repo)
|
|
225
|
+
else:
|
|
226
|
+
raise ValueError(f"Unknown action: {action}")
|
|
227
|
+
|
|
228
|
+
def close(self) -> None:
|
|
229
|
+
"""Close HTTP clients."""
|
|
230
|
+
self.client.close()
|
|
231
|
+
self.async_client.close()
|
|
232
|
+
|
|
233
|
+
async def aclose(self) -> None:
|
|
234
|
+
"""Async close HTTP clients."""
|
|
235
|
+
await self.async_client.aclose()
|
|
236
|
+
self.client.close()
|
kiosque/api/pocket.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from typing import Iterator, Literal, TypedDict
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from ..core.config import config_dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UnknownJSON(TypedDict): ...
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PocketRetrieveEntry(TypedDict):
|
|
12
|
+
item_id: str
|
|
13
|
+
resolved_id: str # useful for duplicates
|
|
14
|
+
given_url: str
|
|
15
|
+
resolved_url: str
|
|
16
|
+
given_title: str
|
|
17
|
+
resolved_title: str
|
|
18
|
+
favorite: Literal[0, 1]
|
|
19
|
+
status: Literal[0, 1, 2]
|
|
20
|
+
excerpt: str # articles only
|
|
21
|
+
is_article: Literal[0, 1]
|
|
22
|
+
has_image: Literal[0, 1, 2]
|
|
23
|
+
has_video: Literal[0, 1, 2]
|
|
24
|
+
word_count: str
|
|
25
|
+
tags: UnknownJSON
|
|
26
|
+
authors: UnknownJSON
|
|
27
|
+
images: UnknownJSON
|
|
28
|
+
videos: UnknownJSON
|
|
29
|
+
time_added: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PocketRetrieveResponse(TypedDict):
|
|
33
|
+
maxActions: int
|
|
34
|
+
status: Literal[0, 1]
|
|
35
|
+
list: dict[str, PocketRetrieveEntry]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PocketAPI:
|
|
39
|
+
json: PocketRetrieveResponse
|
|
40
|
+
|
|
41
|
+
def __init__(self):
|
|
42
|
+
pocket_config = config_dict.get("getpocket.com", None)
|
|
43
|
+
assert pocket_config is not None
|
|
44
|
+
|
|
45
|
+
self.consumer_key = pocket_config.get("consumer_key")
|
|
46
|
+
self.access_token = pocket_config.get("access_token")
|
|
47
|
+
self.client = httpx.Client()
|
|
48
|
+
self.async_client = httpx.AsyncClient()
|
|
49
|
+
|
|
50
|
+
def __len__(self):
|
|
51
|
+
return len(self.json["list"])
|
|
52
|
+
|
|
53
|
+
def __getitem__(self, select):
|
|
54
|
+
for i, elt in enumerate(self.json["list"].values()):
|
|
55
|
+
if i == select:
|
|
56
|
+
return elt
|
|
57
|
+
|
|
58
|
+
def __iter__(self) -> Iterator[PocketRetrieveEntry]:
|
|
59
|
+
yield from self.json["list"].values()
|
|
60
|
+
|
|
61
|
+
def retrieve(self, offset=30) -> PocketRetrieveResponse:
|
|
62
|
+
# https://getpocket.com/developer/docs/v3/retrieve
|
|
63
|
+
r = self.client.post(
|
|
64
|
+
"https://getpocket.com/v3/get",
|
|
65
|
+
json={
|
|
66
|
+
"consumer_key": self.consumer_key,
|
|
67
|
+
"access_token": self.access_token,
|
|
68
|
+
"count": 30,
|
|
69
|
+
"offset": offset,
|
|
70
|
+
"total": 1,
|
|
71
|
+
"sort": "newest",
|
|
72
|
+
},
|
|
73
|
+
headers={
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
"X-Accept": "application/json",
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
r.raise_for_status()
|
|
79
|
+
self.json = r.json()
|
|
80
|
+
return self.json
|
|
81
|
+
|
|
82
|
+
async def async_retrieve(self, offset=0) -> PocketRetrieveResponse:
|
|
83
|
+
r = await self.async_client.post(
|
|
84
|
+
"https://getpocket.com/v3/get",
|
|
85
|
+
json={
|
|
86
|
+
"consumer_key": self.consumer_key,
|
|
87
|
+
"access_token": self.access_token,
|
|
88
|
+
"count": 30,
|
|
89
|
+
"offset": offset,
|
|
90
|
+
"total": 1,
|
|
91
|
+
"sort": "newest",
|
|
92
|
+
"state": "unread",
|
|
93
|
+
},
|
|
94
|
+
headers={
|
|
95
|
+
"Content-Type": "application/json",
|
|
96
|
+
"X-Accept": "application/json",
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
r.raise_for_status()
|
|
100
|
+
self.json = r.json()
|
|
101
|
+
return self.json
|
|
102
|
+
|
|
103
|
+
def action(self, action: str, item_id: str):
|
|
104
|
+
r = self.client.post(
|
|
105
|
+
"https://getpocket.com/v3/send",
|
|
106
|
+
json={
|
|
107
|
+
"consumer_key": self.consumer_key,
|
|
108
|
+
"access_token": self.access_token,
|
|
109
|
+
"actions": [{"action": action, "item_id": item_id}],
|
|
110
|
+
},
|
|
111
|
+
headers={
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
"X-Accept": "application/json",
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
r.raise_for_status()
|
|
117
|
+
return r.json()
|
|
118
|
+
|
|
119
|
+
async def async_action(self, action: str, item_id: str):
|
|
120
|
+
r = await self.async_client.post(
|
|
121
|
+
"https://getpocket.com/v3/send",
|
|
122
|
+
json={
|
|
123
|
+
"consumer_key": self.consumer_key,
|
|
124
|
+
"access_token": self.access_token,
|
|
125
|
+
"actions": [{"action": action, "item_id": item_id}],
|
|
126
|
+
},
|
|
127
|
+
headers={
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
"X-Accept": "application/json",
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
r.raise_for_status()
|
|
133
|
+
return r.json()
|