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.
Files changed (43) hide show
  1. toapi-2.2.2/.claude/settings.local.json +24 -0
  2. {toapi-2.2.1 → toapi-2.2.2}/PKG-INFO +15 -1
  3. {toapi-2.2.1 → toapi-2.2.2}/pyproject.toml +17 -1
  4. toapi-2.2.2/tests/test_toapi.py +123 -0
  5. {toapi-2.2.1 → toapi-2.2.2}/uv.lock +1 -1
  6. toapi-2.2.1/.claude/settings.local.json +0 -9
  7. toapi-2.2.1/tests/test_toapi.py +0 -55
  8. {toapi-2.2.1 → toapi-2.2.2}/.github/workflows/ci.yml +0 -0
  9. {toapi-2.2.1 → toapi-2.2.2}/.gitignore +0 -0
  10. {toapi-2.2.1 → toapi-2.2.2}/.pre-commit-config.yaml +0 -0
  11. {toapi-2.2.1 → toapi-2.2.2}/LICENSE +0 -0
  12. {toapi-2.2.1 → toapi-2.2.2}/README.md +0 -0
  13. {toapi-2.2.1 → toapi-2.2.2}/docs/CNAME +0 -0
  14. {toapi-2.2.1 → toapi-2.2.2}/docs/about/contributing.md +0 -0
  15. {toapi-2.2.1 → toapi-2.2.2}/docs/about/installation.md +0 -0
  16. {toapi-2.2.1 → toapi-2.2.2}/docs/about/license.md +0 -0
  17. {toapi-2.2.1 → toapi-2.2.2}/docs/about/release-notes.md +0 -0
  18. {toapi-2.2.1 → toapi-2.2.2}/docs/diagram.png +0 -0
  19. {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/introducing-1.png +0 -0
  20. {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/introducing-2.png +0 -0
  21. {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/introducing-3.png +0 -0
  22. {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/introducing-4.png +0 -0
  23. {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/runinglog.png +0 -0
  24. {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/runningitems.png +0 -0
  25. {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/runningresult.png +0 -0
  26. {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/runningstatus.png +0 -0
  27. {toapi-2.2.1 → toapi-2.2.2}/docs/imgs/step-0-1.png +0 -0
  28. {toapi-2.2.1 → toapi-2.2.2}/docs/index.md +0 -0
  29. {toapi-2.2.1 → toapi-2.2.2}/docs/logo.png +0 -0
  30. {toapi-2.2.1 → toapi-2.2.2}/docs/quickstart.md +0 -0
  31. {toapi-2.2.1 → toapi-2.2.2}/docs/topics/api.md +0 -0
  32. {toapi-2.2.1 → toapi-2.2.2}/docs/topics/item.md +0 -0
  33. {toapi-2.2.1 → toapi-2.2.2}/docs/topics/selector.md +0 -0
  34. {toapi-2.2.1 → toapi-2.2.2}/examples/click/app.py +0 -0
  35. {toapi-2.2.1 → toapi-2.2.2}/examples/click/static/main.js +0 -0
  36. {toapi-2.2.1 → toapi-2.2.2}/examples/click/templates/index.html +0 -0
  37. {toapi-2.2.1 → toapi-2.2.2}/examples/hackernews_page.py +0 -0
  38. {toapi-2.2.1 → toapi-2.2.2}/mkdocs.yml +0 -0
  39. {toapi-2.2.1 → toapi-2.2.2}/toapi/__init__.py +0 -0
  40. {toapi-2.2.1 → toapi-2.2.2}/toapi/api.py +0 -0
  41. {toapi-2.2.1 → toapi-2.2.2}/toapi/cli.py +0 -0
  42. {toapi-2.2.1 → toapi-2.2.2}/toapi/item.py +0 -0
  43. {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.1
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.1"
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)
@@ -1160,7 +1160,7 @@ wheels = [
1160
1160
 
1161
1161
  [[package]]
1162
1162
  name = "toapi"
1163
- version = "2.2.1"
1163
+ version = "2.2.2"
1164
1164
  source = { editable = "." }
1165
1165
  dependencies = [
1166
1166
  { name = "charset-normalizer" },
@@ -1,9 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(uv run *)",
5
- "Bash(uv sync *)",
6
- "Bash(uv build *)"
7
- ]
8
- }
9
- }
@@ -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