toapi 2.2.1__tar.gz → 2.2.2__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.
- toapi-2.2.2/.claude/settings.local.json +24 -0
- {toapi-2.2.1 → toapi-2.2.2}/PKG-INFO +15 -1
- {toapi-2.2.1 → toapi-2.2.2}/pyproject.toml +17 -1
- toapi-2.2.2/tests/test_toapi.py +123 -0
- {toapi-2.2.1 → toapi-2.2.2}/uv.lock +1 -1
- toapi-2.2.1/.claude/settings.local.json +0 -9
- toapi-2.2.1/tests/test_toapi.py +0 -55
- {toapi-2.2.1 → toapi-2.2.2}/.github/workflows/ci.yml +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/.gitignore +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/.pre-commit-config.yaml +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/LICENSE +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/README.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/CNAME +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/about/contributing.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/about/installation.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/about/license.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/about/release-notes.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/diagram.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/introducing-1.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/introducing-2.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/introducing-3.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/introducing-4.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/runinglog.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/runningitems.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/runningresult.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/runningstatus.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/step-0-1.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/index.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/logo.png +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/quickstart.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/topics/api.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/topics/item.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/docs/topics/selector.md +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/examples/click/app.py +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/examples/click/static/main.js +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/examples/click/templates/index.html +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/examples/hackernews_page.py +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/mkdocs.yml +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/toapi/__init__.py +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/toapi/api.py +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/toapi/cli.py +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/toapi/item.py +0 -0
- {toapi-2.2.1 → toapi-2.2.2}/toapi/log.py +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(uv run *)",
|
|
5
|
+
"Bash(uv sync *)",
|
|
6
|
+
"Bash(uv build *)",
|
|
7
|
+
"Bash(git add *)",
|
|
8
|
+
"Bash(curl -sI https://pypi.org/project/toapi/2.2.1/)",
|
|
9
|
+
"Bash(curl -s \"https://pypi.org/simple/toapi/\")",
|
|
10
|
+
"Bash(curl -sI -L https://pypi.org/project/toapi/2.2.1/)",
|
|
11
|
+
"Bash(curl -s https://pypi.org/pypi/toapi/2.2.1/json)",
|
|
12
|
+
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('version:', d['info']['version']\\); print\\('files:', [f['filename'] for f in d['urls']]\\)\")",
|
|
13
|
+
"Bash(gh run *)",
|
|
14
|
+
"Bash(git commit -m ' *)",
|
|
15
|
+
"Bash(git push *)",
|
|
16
|
+
"Bash(curl -sI \"https://github.com/elliotgao2/toapi/actions/workflows/ci.yml/badge.svg\")",
|
|
17
|
+
"Bash(curl -s \"https://img.shields.io/pypi/v/toapi.svg\")",
|
|
18
|
+
"Bash(curl -s \"https://img.shields.io/pypi/pyversions/toapi.svg\")",
|
|
19
|
+
"Bash(curl -s \"https://img.shields.io/pypi/l/toapi.svg\")",
|
|
20
|
+
"Bash(curl -s https://pypi.org/pypi/toapi/json)",
|
|
21
|
+
"Bash(python3 -c ' *)"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: toapi
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Every web site provides APIs.
|
|
5
5
|
Project-URL: homepage, https://github.com/gaojiuli/toapi
|
|
6
6
|
Project-URL: repository, https://github.com/gaojiuli/toapi
|
|
@@ -8,6 +8,20 @@ Project-URL: documentation, https://gaojiuli.github.io/toapi/
|
|
|
8
8
|
Author-email: Elliot Gao <gaojiuli@gmail.com>
|
|
9
9
|
License: MIT
|
|
10
10
|
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Framework :: Flask
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: Text Processing :: Markup :: HTML
|
|
11
25
|
Requires-Python: >=3.10
|
|
12
26
|
Requires-Dist: charset-normalizer>=3.3
|
|
13
27
|
Requires-Dist: click>=8.1
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "toapi"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.2"
|
|
4
4
|
description = "Every web site provides APIs."
|
|
5
5
|
authors = [{ name = "Elliot Gao", email = "gaojiuli@gmail.com" }]
|
|
6
6
|
license = { text = "MIT" }
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Framework :: Flask",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
"Topic :: Text Processing :: Markup :: HTML",
|
|
24
|
+
]
|
|
9
25
|
dependencies = [
|
|
10
26
|
"colorama>=0.4.6",
|
|
11
27
|
"charset-normalizer>=3.3",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from htmlparsing import Attr, Text
|
|
5
|
+
from webtest import TestApp as App
|
|
6
|
+
|
|
7
|
+
from toapi import Api, Item
|
|
8
|
+
from toapi.cli import cli
|
|
9
|
+
|
|
10
|
+
SAMPLE_HTML = """
|
|
11
|
+
<html><body>
|
|
12
|
+
<table>
|
|
13
|
+
<tr class="athing">
|
|
14
|
+
<td><span class="titleline"><a href="https://example.com/1">First story</a></span></td>
|
|
15
|
+
</tr>
|
|
16
|
+
<tr class="athing">
|
|
17
|
+
<td><span class="titleline"><a href="https://example.com/2">Second story</a></span></td>
|
|
18
|
+
</tr>
|
|
19
|
+
</table>
|
|
20
|
+
<a class="morelink" href="news?p=2">More</a>
|
|
21
|
+
</body></html>
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FakeResponse:
|
|
26
|
+
content = SAMPLE_HTML.encode("utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def fake_get():
|
|
31
|
+
with patch("toapi.api.requests.get", return_value=FakeResponse()) as mock:
|
|
32
|
+
yield mock
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_list_item_parses_each_row(fake_get):
|
|
36
|
+
api = Api()
|
|
37
|
+
|
|
38
|
+
@api.site("https://example.com")
|
|
39
|
+
@api.list(".athing")
|
|
40
|
+
@api.route("/posts", "/news")
|
|
41
|
+
class Post(Item):
|
|
42
|
+
title = Text(".titleline > a")
|
|
43
|
+
url = Attr(".titleline > a", "href")
|
|
44
|
+
|
|
45
|
+
app = App(api.app)
|
|
46
|
+
body = app.get("/posts").json
|
|
47
|
+
assert body["Post"] == [
|
|
48
|
+
{"title": "First story", "url": "https://example.com/1"},
|
|
49
|
+
{"title": "Second story", "url": "https://example.com/2"},
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_detail_item_returns_single_dict(fake_get):
|
|
54
|
+
api = Api()
|
|
55
|
+
|
|
56
|
+
@api.site("https://example.com")
|
|
57
|
+
@api.route("/page", "/news")
|
|
58
|
+
class Page(Item):
|
|
59
|
+
next_page = Attr(".morelink", "href")
|
|
60
|
+
|
|
61
|
+
app = App(api.app)
|
|
62
|
+
body = app.get("/page").json
|
|
63
|
+
assert body["Page"] == {"next_page": "news?p=2"}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_clean_method_transforms_field(fake_get):
|
|
67
|
+
api = Api()
|
|
68
|
+
|
|
69
|
+
@api.site("https://example.com")
|
|
70
|
+
@api.route("/page", "/news")
|
|
71
|
+
class Page(Item):
|
|
72
|
+
next_page = Attr(".morelink", "href")
|
|
73
|
+
|
|
74
|
+
def clean_next_page(self, value):
|
|
75
|
+
return f"/wrapped/{value}"
|
|
76
|
+
|
|
77
|
+
app = App(api.app)
|
|
78
|
+
body = app.get("/page").json
|
|
79
|
+
assert body["Page"]["next_page"] == "/wrapped/news?p=2"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_multiple_items_merge_into_one_response(fake_get):
|
|
83
|
+
api = Api()
|
|
84
|
+
|
|
85
|
+
@api.site("https://example.com")
|
|
86
|
+
@api.list(".athing")
|
|
87
|
+
@api.route("/feed", "/news")
|
|
88
|
+
class Post(Item):
|
|
89
|
+
title = Text(".titleline > a")
|
|
90
|
+
|
|
91
|
+
@api.site("https://example.com")
|
|
92
|
+
@api.route("/feed", "/news")
|
|
93
|
+
class Pager(Item):
|
|
94
|
+
next_page = Attr(".morelink", "href")
|
|
95
|
+
|
|
96
|
+
app = App(api.app)
|
|
97
|
+
body = app.get("/feed").json
|
|
98
|
+
assert len(body["Post"]) == 2
|
|
99
|
+
assert body["Pager"] == {"next_page": "news?p=2"}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_handler_returns_500_when_parsing_fails(fake_get):
|
|
103
|
+
api = Api()
|
|
104
|
+
|
|
105
|
+
@api.site("https://example.com")
|
|
106
|
+
@api.list(".athing")
|
|
107
|
+
@api.route("/posts", "/news")
|
|
108
|
+
class Post(Item):
|
|
109
|
+
missing = Attr(".does-not-exist", "href")
|
|
110
|
+
|
|
111
|
+
app = App(api.app)
|
|
112
|
+
with pytest.raises(Exception):
|
|
113
|
+
app.get("/posts")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_cli_is_importable_and_callable():
|
|
117
|
+
assert callable(cli)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_run_exits_on_invalid_port():
|
|
121
|
+
api = Api()
|
|
122
|
+
with pytest.raises(SystemExit):
|
|
123
|
+
api.run(port=-1)
|
toapi-2.2.1/tests/test_toapi.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from flask import request
|
|
3
|
-
from htmlparsing import Attr, Text
|
|
4
|
-
from webtest import TestApp as App
|
|
5
|
-
|
|
6
|
-
from toapi import Api, Item
|
|
7
|
-
from toapi.cli import cli
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_api():
|
|
11
|
-
api = Api()
|
|
12
|
-
|
|
13
|
-
@api.site("https://news.ycombinator.com")
|
|
14
|
-
@api.list(".athing")
|
|
15
|
-
@api.route("/posts?page={page}", "/news?p={page}")
|
|
16
|
-
@api.route("/posts", "/news?p=1")
|
|
17
|
-
class Post(Item):
|
|
18
|
-
url = Attr(".storylink", "href")
|
|
19
|
-
title = Text(".storylink")
|
|
20
|
-
|
|
21
|
-
@api.site("https://news.ycombinator.com")
|
|
22
|
-
@api.route("/posts?page={page}", "/news?p={page}")
|
|
23
|
-
@api.route("/posts", "/news?p=1")
|
|
24
|
-
class Page(Item):
|
|
25
|
-
next_page = Attr(".morelink", "href")
|
|
26
|
-
|
|
27
|
-
def clean_next_page(self, value):
|
|
28
|
-
return api.convert_string(
|
|
29
|
-
"/" + value,
|
|
30
|
-
"/news?p={page}",
|
|
31
|
-
request.host_url.strip("/") + "/posts?page={page}",
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
app = App(api.app)
|
|
35
|
-
with pytest.raises(SystemExit):
|
|
36
|
-
api.run(port=-1)
|
|
37
|
-
app.get("/posts?page=1")
|
|
38
|
-
app.get("/posts?page=1")
|
|
39
|
-
print(cli.__dict__)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_error():
|
|
43
|
-
api = Api()
|
|
44
|
-
|
|
45
|
-
@api.site("https://news.ycombinator.com")
|
|
46
|
-
@api.list(".athing")
|
|
47
|
-
@api.route("/posts?page={page}", "/news?p={page}")
|
|
48
|
-
@api.route("/posts", "/news?p=1")
|
|
49
|
-
class Post(Item):
|
|
50
|
-
url = Attr(".storylink", "no this attribute")
|
|
51
|
-
title = Text(".storylink")
|
|
52
|
-
|
|
53
|
-
app = App(api.app)
|
|
54
|
-
with pytest.raises(Exception):
|
|
55
|
-
app.get("/posts?page=1")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|