wxpath 0.2.0__py3-none-any.whl → 0.3.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.
- wxpath/cli.py +52 -12
- wxpath/core/ops.py +163 -129
- wxpath/core/parser.py +559 -280
- wxpath/core/runtime/engine.py +133 -42
- wxpath/core/runtime/helpers.py +0 -7
- wxpath/hooks/registry.py +29 -17
- wxpath/http/client/crawler.py +46 -11
- wxpath/http/client/request.py +6 -3
- wxpath/http/client/response.py +1 -1
- wxpath/http/policy/robots.py +82 -0
- {wxpath-0.2.0.dist-info → wxpath-0.3.0.dist-info}/METADATA +84 -37
- {wxpath-0.2.0.dist-info → wxpath-0.3.0.dist-info}/RECORD +16 -16
- wxpath/core/errors.py +0 -134
- {wxpath-0.2.0.dist-info → wxpath-0.3.0.dist-info}/WHEEL +0 -0
- {wxpath-0.2.0.dist-info → wxpath-0.3.0.dist-info}/entry_points.txt +0 -0
- {wxpath-0.2.0.dist-info → wxpath-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {wxpath-0.2.0.dist-info → wxpath-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wxpath
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: wxpath - a declarative web crawler and data extractor
|
|
5
5
|
Author-email: Rodrigo Palacios <rodrigopala91@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Requires-Dist: requests>=2.0
|
|
11
10
|
Requires-Dist: lxml>=4.0
|
|
12
11
|
Requires-Dist: elementpath<=5.0.3,>=5.0.0
|
|
13
12
|
Requires-Dist: aiohttp<=3.12.15,>=3.8.0
|
|
@@ -18,12 +17,13 @@ Provides-Extra: dev
|
|
|
18
17
|
Requires-Dist: ruff; extra == "dev"
|
|
19
18
|
Dynamic: license-file
|
|
20
19
|
|
|
20
|
+
# **wxpath** - declarative web crawling with XPath
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
[](https://www.python.org/downloads/release/python-3100/)
|
|
23
23
|
|
|
24
|
-
**wxpath** is a declarative web crawler where traversal is expressed directly in XPath. Instead of writing imperative crawl loops, you describe what to follow and what to extract in a single expression. **wxpath**
|
|
24
|
+
**wxpath** is a declarative web crawler where traversal is expressed directly in XPath. Instead of writing imperative crawl loops, wxpath lets you describe what to follow and what to extract in a single expression. **wxpath** executes that expression concurrently, breadth-first-*ish*, and streams results as they are discovered.
|
|
25
25
|
|
|
26
|
-
By introducing the `url(...)` operator and the `///` syntax,
|
|
26
|
+
By introducing the `url(...)` operator and the `///` syntax, wxpath's engine is able to perform deep (or paginated) web crawling and extraction.
|
|
27
27
|
|
|
28
28
|
NOTE: This project is in early development. Core concepts are stable, but the API and features may change. Please report issues - in particular, deadlocked crawls or unexpected behavior - and any features you'd like to see (no guarantee they'll be implemented).
|
|
29
29
|
|
|
@@ -31,19 +31,22 @@ NOTE: This project is in early development. Core concepts are stable, but the AP
|
|
|
31
31
|
## Contents
|
|
32
32
|
|
|
33
33
|
- [Example](#example)
|
|
34
|
-
- [
|
|
34
|
+
- [Language Design](DESIGN.md)
|
|
35
|
+
- [`url(...)` and `///url(...)` Explained](#url-and-url-explained)
|
|
35
36
|
- [General flow](#general-flow)
|
|
36
37
|
- [Asynchronous Crawling](#asynchronous-crawling)
|
|
38
|
+
- [Polite Crawling](#polite-crawling)
|
|
37
39
|
- [Output types](#output-types)
|
|
38
|
-
- [XPath 3.1
|
|
40
|
+
- [XPath 3.1](#xpath-31-by-default)
|
|
39
41
|
- [CLI](#cli)
|
|
40
42
|
- [Hooks (Experimental)](#hooks-experimental)
|
|
41
43
|
- [Install](#install)
|
|
42
|
-
- [More Examples](
|
|
44
|
+
- [More Examples](EXAMPLES.md)
|
|
43
45
|
- [Comparisons](#comparisons)
|
|
44
46
|
- [Advanced: Engine & Crawler Configuration](#advanced-engine--crawler-configuration)
|
|
45
47
|
- [Project Philosophy](#project-philosophy)
|
|
46
48
|
- [Warnings](#warnings)
|
|
49
|
+
- [Commercial support / consulting](#commercial-support--consulting)
|
|
47
50
|
- [License](#license)
|
|
48
51
|
|
|
49
52
|
|
|
@@ -52,33 +55,35 @@ NOTE: This project is in early development. Core concepts are stable, but the AP
|
|
|
52
55
|
```python
|
|
53
56
|
import wxpath
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
# Crawl, extract fields, build a knowledge graph
|
|
59
|
+
path_expr = """
|
|
56
60
|
url('https://en.wikipedia.org/wiki/Expression_language')
|
|
57
61
|
///url(//main//a/@href[starts-with(., '/wiki/') and not(contains(., ':'))])
|
|
58
62
|
/map{
|
|
59
|
-
'title':(//span[contains(@class, "mw-page-title-main")]/text())[1],
|
|
60
|
-
'url':string(base-uri(.)),
|
|
61
|
-
'short_description':
|
|
63
|
+
'title': (//span[contains(@class, "mw-page-title-main")]/text())[1] ! string(.),
|
|
64
|
+
'url': string(base-uri(.)),
|
|
65
|
+
'short_description': //div[contains(@class, 'shortdescription')]/text() ! string(.),
|
|
66
|
+
'forward_links': //div[@id="mw-content-text"]//a/@href ! string(.)
|
|
62
67
|
}
|
|
63
68
|
"""
|
|
64
69
|
|
|
65
|
-
for item in wxpath.wxpath_async_blocking_iter(
|
|
70
|
+
for item in wxpath.wxpath_async_blocking_iter(path_expr, max_depth=1):
|
|
66
71
|
print(item)
|
|
67
72
|
```
|
|
68
73
|
|
|
69
74
|
Output:
|
|
70
75
|
|
|
71
76
|
```python
|
|
72
|
-
map{'title':
|
|
73
|
-
map{'title':
|
|
74
|
-
map{'title':
|
|
75
|
-
|
|
76
|
-
map{'title': TextNode('Data Analysis Expressions'), 'url': 'https://en.wikipedia.org/wiki/Data_Analysis_Expressions', 'short_description': TextNode('Formula and data query language')}
|
|
77
|
-
map{'title': TextNode('Domain knowledge'), 'url': 'https://en.wikipedia.org/wiki/Domain_knowledge', 'short_description': TextNode('Specialist knowledge within a specific field')}
|
|
78
|
-
map{'title': TextNode('Rights Expression Language'), 'url': 'https://en.wikipedia.org/wiki/Rights_Expression_Language', 'short_description': TextNode('Machine-processable language used to express intellectual property rights (such as copyright)')}
|
|
79
|
-
map{'title': TextNode('Computer science'), 'url': 'https://en.wikipedia.org/wiki/Computer_science', 'short_description': TextNode('Study of computation')}
|
|
77
|
+
map{'title': 'Computer language', 'url': 'https://en.wikipedia.org/wiki/Computer_language', 'short_description': 'Formal language for communicating with a computer', 'forward_links': ['/wiki/Formal_language', '/wiki/Communication', ...]}
|
|
78
|
+
map{'title': 'Advanced Boolean Expression Language', 'url': 'https://en.wikipedia.org/wiki/Advanced_Boolean_Expression_Language', 'short_description': 'Hardware description language and software', 'forward_links': ['/wiki/File:ABEL_HDL_example_SN74162.png', '/wiki/Hardware_description_language', ...]}
|
|
79
|
+
map{'title': 'Machine-readable medium and data', 'url': 'https://en.wikipedia.org/wiki/Machine_readable', 'short_description': 'Medium capable of storing data in a format readable by a machine', 'forward_links': ['/wiki/File:EAN-13-ISBN-13.svg', '/wiki/ISBN', ...]}
|
|
80
|
+
...
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
**Note:** Some sites (including Wikipedia) may block requests without proper headers.
|
|
84
|
+
See [Advanced: Engine & Crawler Configuration](#advanced-engine--crawler-configuration) to set a custom `User-Agent`.
|
|
85
|
+
|
|
86
|
+
|
|
82
87
|
The above expression does the following:
|
|
83
88
|
|
|
84
89
|
1. Starts at the specified URL, `https://en.wikipedia.org/wiki/Expression_language`.
|
|
@@ -92,18 +97,23 @@ The above expression does the following:
|
|
|
92
97
|
## `url(...)` and `///url(...)` Explained
|
|
93
98
|
|
|
94
99
|
- `url(...)` is a custom operator that fetches the content of the user-specified or internally generated URL and returns it as an `lxml.html.HtmlElement` for further XPath processing.
|
|
95
|
-
- `///url(...)` indicates
|
|
100
|
+
- `///url(...)` indicates a deep crawl. It tells the runtime engine to continue following links up to the specified `max_depth`. Unlike repeated `url()` hops, it allows a single expression to describe deeper graph exploration. WARNING: Use with caution and constraints (via `max_depth` or XPath predicates) to avoid traversal explosion.
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## Language Design
|
|
104
|
+
|
|
105
|
+
See [DESIGN.md](DESIGN.md) for details of the language design. You will see the core concepts and design the language from the ground up.
|
|
96
106
|
|
|
97
107
|
|
|
98
108
|
## General flow
|
|
99
109
|
|
|
100
110
|
**wxpath** evaluates an expression as a list of traversal and extraction steps (internally referred to as `Segment`s).
|
|
101
111
|
|
|
102
|
-
`url(...)` creates crawl tasks either statically (via a fixed URL) or dynamically (via a URL derived from the XPath expression). **URLs are deduplicated globally,
|
|
112
|
+
`url(...)` creates crawl tasks either statically (via a fixed URL) or dynamically (via a URL derived from the XPath expression). **URLs are deduplicated globally, on a best-effort basis - not per-depth**.
|
|
103
113
|
|
|
104
114
|
XPath segments operate on fetched documents (fetched via the immediately preceding `url(...)` operations).
|
|
105
115
|
|
|
106
|
-
`///url(...)` indicates
|
|
116
|
+
`///url(...)` indicates deep crawling - it proceeds breadth-first-*ish* up to `max_depth`.
|
|
107
117
|
|
|
108
118
|
Results are yielded as soon as they are ready.
|
|
109
119
|
|
|
@@ -128,7 +138,7 @@ asyncio.run(main())
|
|
|
128
138
|
|
|
129
139
|
### Blocking, Concurrent Requests
|
|
130
140
|
|
|
131
|
-
**wxpath** also
|
|
141
|
+
**wxpath** also provides an asyncio-in-sync API, allowing you to crawl multiple pages concurrently while maintaining the simplicity of synchronous code. This is particularly useful for crawls in strictly synchronous execution environments (i.e., not inside an `asyncio` event loop) where performance is a concern.
|
|
132
142
|
|
|
133
143
|
```python
|
|
134
144
|
from wxpath import wxpath_async_blocking_iter
|
|
@@ -137,10 +147,14 @@ path_expr = "url('https://en.wikipedia.org/wiki/Expression_language')///url(//@h
|
|
|
137
147
|
items = list(wxpath_async_blocking_iter(path_expr, max_depth=1))
|
|
138
148
|
```
|
|
139
149
|
|
|
150
|
+
## Polite Crawling
|
|
151
|
+
|
|
152
|
+
**wxpath** respects [robots.txt](https://en.wikipedia.org/wiki/Robots_exclusion_standard) by default via the `WXPathEngine(..., robotstxt=True)` constructor.
|
|
153
|
+
|
|
140
154
|
|
|
141
155
|
## Output types
|
|
142
156
|
|
|
143
|
-
The wxpath Python API yields structured objects
|
|
157
|
+
The wxpath Python API yields structured objects.
|
|
144
158
|
|
|
145
159
|
Depending on the expression, results may include:
|
|
146
160
|
|
|
@@ -188,10 +202,11 @@ path_expr = """
|
|
|
188
202
|
|
|
189
203
|
The following example demonstrates how to crawl Wikipedia starting from the "Expression language" page, extract links to other wiki pages, and retrieve specific fields from each linked page.
|
|
190
204
|
|
|
191
|
-
|
|
205
|
+
NOTE: Due to the everchanging nature of web content, the output may vary over time.
|
|
192
206
|
```bash
|
|
193
|
-
> wxpath --depth 1
|
|
194
|
-
|
|
207
|
+
> wxpath --depth 1 \
|
|
208
|
+
--header "User-Agent: my-app/0.1 (contact: you@example.com)" \
|
|
209
|
+
"url('https://en.wikipedia.org/wiki/Expression_language') \
|
|
195
210
|
///url(//div[@id='mw-content-text']//a/@href[starts-with(., '/wiki/') \
|
|
196
211
|
and not(matches(@href, '^(?:/wiki/)?(?:Wikipedia|File|Template|Special|Template_talk|Help):'))]) \
|
|
197
212
|
/map{ \
|
|
@@ -212,6 +227,18 @@ WARNING: Due to the everchanging nature of web content, the output may vary over
|
|
|
212
227
|
{"title": "Computer science", "short_description": "Study of computation", "url": "https://en.wikipedia.org/wiki/Computer_science", "backlink": "https://en.wikipedia.org/wiki/Expression_language", "depth": 1.0}
|
|
213
228
|
```
|
|
214
229
|
|
|
230
|
+
Command line options:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
--depth <depth> Max crawl depth
|
|
234
|
+
--verbose [true|false] Provides superficial CLI information
|
|
235
|
+
--debug [true|false] Provides verbose runtime output and information
|
|
236
|
+
--concurrency <concurrency> Number of concurrent fetches
|
|
237
|
+
--concurrency-per-host <concurrency> Number of concurrent fetches per host
|
|
238
|
+
--header "Key:Value" Add a custom header (e.g., 'Key:Value'). Can be used multiple times.
|
|
239
|
+
--respect-robots [true|false] (Default: True) Respects robots.txt
|
|
240
|
+
```
|
|
241
|
+
|
|
215
242
|
|
|
216
243
|
## Hooks (Experimental)
|
|
217
244
|
|
|
@@ -257,6 +284,8 @@ hooks.register(hooks.JSONLWriter)
|
|
|
257
284
|
|
|
258
285
|
## Install
|
|
259
286
|
|
|
287
|
+
Requires Python 3.10+.
|
|
288
|
+
|
|
260
289
|
```
|
|
261
290
|
pip install wxpath
|
|
262
291
|
```
|
|
@@ -285,13 +314,20 @@ crawler = Crawler(
|
|
|
285
314
|
concurrency=8,
|
|
286
315
|
per_host=2,
|
|
287
316
|
timeout=10,
|
|
317
|
+
respect_robots=False,
|
|
318
|
+
headers={
|
|
319
|
+
"User-Agent": "my-app/0.1.0 (contact: you@example.com)", # Sites like Wikipedia will appreciate this
|
|
320
|
+
},
|
|
288
321
|
)
|
|
289
322
|
|
|
290
323
|
# If `crawler` is not specified, a default Crawler will be created with
|
|
291
|
-
# the provided concurrency and
|
|
324
|
+
# the provided concurrency, per_host, and respect_robots values, or with defaults.
|
|
292
325
|
engine = WXPathEngine(
|
|
293
|
-
# concurrency=16,
|
|
294
|
-
# per_host=8,
|
|
326
|
+
# concurrency: int = 16,
|
|
327
|
+
# per_host: int = 8,
|
|
328
|
+
# respect_robots: bool = True,
|
|
329
|
+
# allowed_response_codes: set[int] = {200},
|
|
330
|
+
# allow_redirects: bool = True,
|
|
295
331
|
crawler=crawler,
|
|
296
332
|
)
|
|
297
333
|
|
|
@@ -305,7 +341,7 @@ items = list(wxpath_async_blocking_iter(path_expr, max_depth=1, engine=engine))
|
|
|
305
341
|
|
|
306
342
|
### Principles
|
|
307
343
|
|
|
308
|
-
- Enable declarative,
|
|
344
|
+
- Enable declarative, crawling and scraping without boilerplate
|
|
309
345
|
- Stay lightweight and composable
|
|
310
346
|
- Asynchronous support for high-performance crawls
|
|
311
347
|
|
|
@@ -316,22 +352,33 @@ items = list(wxpath_async_blocking_iter(path_expr, max_depth=1, engine=engine))
|
|
|
316
352
|
- Requests are performed concurrently.
|
|
317
353
|
- Results are streamed as soon as they are available.
|
|
318
354
|
|
|
319
|
-
###
|
|
355
|
+
### Limitations (for now)
|
|
356
|
+
|
|
357
|
+
The following features are not yet supported:
|
|
320
358
|
|
|
321
|
-
- Strict result ordering
|
|
322
359
|
- Persistent scheduling or crawl resumption
|
|
323
360
|
- Automatic proxy rotation
|
|
324
361
|
- Browser-based rendering (JavaScript execution)
|
|
362
|
+
- Strict result ordering
|
|
325
363
|
|
|
326
364
|
|
|
327
365
|
## WARNINGS!!!
|
|
328
366
|
|
|
329
367
|
- Be respectful when crawling websites. A scrapy-inspired throttler is enabled by default.
|
|
330
|
-
-
|
|
368
|
+
- Deep crawls (`///`) require user discipline to avoid unbounded expansion (traversal explosion).
|
|
331
369
|
- Deadlocks and hangs are possible in certain situations (e.g., all tasks waiting on blocked requests). Please report issues if you encounter such behavior.
|
|
332
370
|
- Consider using timeouts, `max_depth`, and XPath predicates and filters to limit crawl scope.
|
|
333
371
|
|
|
334
372
|
|
|
373
|
+
## Commercial support / consulting
|
|
374
|
+
|
|
375
|
+
If you want help building or operating crawlers/data feeds with wxpath (extraction, scheduling, monitoring, breakage fixes) or other web-scraping needs, please contact me at: rodrigopala91@gmail.com.
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
### Donate
|
|
379
|
+
|
|
380
|
+
If you like wxpath and want to support its development, please consider [donating](https://www.paypal.com/donate/?business=WDNDK6J6PJEXY&no_recurring=0&item_name=Thanks+for+using+wxpath%21+Donations+fund+development%2C+docs%2C+and+bug+fixes.+If+wxpath+saved+you+time%2C+a+small+contribution+helps%21¤cy_code=USD).
|
|
381
|
+
|
|
335
382
|
## License
|
|
336
383
|
|
|
337
384
|
MIT
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
wxpath/__init__.py,sha256=w1hFE_VSIYq_TSFLoPfp6MJbG1sA6BeChX6PYsXIK4o,265
|
|
2
|
-
wxpath/cli.py,sha256=
|
|
2
|
+
wxpath/cli.py,sha256=GJ4vAax5DlpxczZ_eLetlfRwa177VFKo2LHv09X-0eo,2799
|
|
3
3
|
wxpath/patches.py,sha256=u0dOL-K-gvdO9SJvzGrqR9Zou6XduWjl6R7mzIcZtJg,2130
|
|
4
4
|
wxpath/core/__init__.py,sha256=U9_In2iRaZrpiIVavIli1M59gCB6Kn1en-1Fza-qIiI,257
|
|
5
5
|
wxpath/core/dom.py,sha256=X0L3n8jRfO5evEypDaJTD-NQ3cLXWvnEUVERAHo3vV0,701
|
|
6
|
-
wxpath/core/errors.py,sha256=q56Gs5JJSC4HKImUtdZhOHcqe8XsoIrVhsaaoJ2qhCQ,4198
|
|
7
6
|
wxpath/core/models.py,sha256=3KYt-UwfLY2FlSRUHeA_getnYaNUMPW9wRrl2CRbPso,1611
|
|
8
|
-
wxpath/core/ops.py,sha256=
|
|
9
|
-
wxpath/core/parser.py,sha256=
|
|
7
|
+
wxpath/core/ops.py,sha256=PTjX6c4QvCqGaByYYqaK4dte5iWO3lZzgqGrMXp6f6g,9727
|
|
8
|
+
wxpath/core/parser.py,sha256=WfjQNixBz7nWtX2O0t19MOhUJmzGMg8Qol40P6oC8zc,18827
|
|
10
9
|
wxpath/core/runtime/__init__.py,sha256=_iCgkIWxXvxzQcenHOsjYGsk74HboTIYWOtgM8GtCyc,86
|
|
11
|
-
wxpath/core/runtime/engine.py,sha256=
|
|
12
|
-
wxpath/core/runtime/helpers.py,sha256=
|
|
10
|
+
wxpath/core/runtime/engine.py,sha256=069ITKDXcHss__AwaYf0VSfliCNB49yZbnW2v3xEZO0,14512
|
|
11
|
+
wxpath/core/runtime/helpers.py,sha256=M1i4BryCktAxeboa4LOXMTNiKVCJLDBD-KpWCQXadpw,1434
|
|
13
12
|
wxpath/hooks/__init__.py,sha256=9JG63e4z_8CZLWugFcY786hebaEEPZ5FmZhyDHat-98,294
|
|
14
13
|
wxpath/hooks/builtin.py,sha256=GJ4w1C9djWNzAmAA3U0qI9OoCOeC5R8tEGtWXJVHSYs,4125
|
|
15
|
-
wxpath/hooks/registry.py,sha256
|
|
14
|
+
wxpath/hooks/registry.py,sha256=-D11f_mMboeVAH8qsTkbKTQ0aGNaQ7F6zbXDsOIYxN0,4513
|
|
16
15
|
wxpath/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
16
|
wxpath/http/stats.py,sha256=FrXbFrnms113Gapf-Z5WiD5qaNiJ0XuOqjSQhwXfuEo,3172
|
|
18
17
|
wxpath/http/client/__init__.py,sha256=QpdmqzcznUeuFvT3IIo-LmBUUHEa2BDq9sHGAHJnDLI,202
|
|
19
|
-
wxpath/http/client/crawler.py,sha256=
|
|
20
|
-
wxpath/http/client/request.py,sha256=
|
|
21
|
-
wxpath/http/client/response.py,sha256=
|
|
18
|
+
wxpath/http/client/crawler.py,sha256=YlE469UqMck0wqRd6J9kNxm5G9BCbE_x5O6MROwmcaE,8742
|
|
19
|
+
wxpath/http/client/request.py,sha256=LF_OIXetfouyE5GwEqp0cya0oMAZouKRPNFRFGscQS8,1050
|
|
20
|
+
wxpath/http/client/response.py,sha256=z9LQPnDN-NZRnQpIKozaWCqgpRejc6nixCr_XaPyqUQ,334
|
|
22
21
|
wxpath/http/policy/backoff.py,sha256=NwdUR6bRe1RtUGSJOktj-p8IyC1l9xu_-Aa_Gj_u5sw,321
|
|
23
22
|
wxpath/http/policy/retry.py,sha256=WSrQfCy1F7IcXFpVGDi4HTphNhFq12p4DaMO0_4dgrw,982
|
|
23
|
+
wxpath/http/policy/robots.py,sha256=vllXX9me78YB6yrDdpH_bwyuR5QoC9uveGEl8PmHM9Q,3134
|
|
24
24
|
wxpath/http/policy/throttler.py,sha256=wydMFV-0mxpHSI5iYkLfE78oY4z_fF8jW9MqCeb8G54,3014
|
|
25
25
|
wxpath/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
wxpath/util/logging.py,sha256=oQi8sp7yKWgXkkcJ4U4WHp7TyBCQiK4VhSXOSb8pGw0,2965
|
|
27
27
|
wxpath/util/serialize.py,sha256=uUs4C9VErpFd97smBM2bRWo2nW25kCgKdsMrVtVxhg8,575
|
|
28
|
-
wxpath-0.
|
|
29
|
-
wxpath-0.
|
|
30
|
-
wxpath-0.
|
|
31
|
-
wxpath-0.
|
|
32
|
-
wxpath-0.
|
|
33
|
-
wxpath-0.
|
|
28
|
+
wxpath-0.3.0.dist-info/licenses/LICENSE,sha256=AVBZLhdWmqxm-f-dy5prVB1E-solHWoP2EXEIV_o-00,1076
|
|
29
|
+
wxpath-0.3.0.dist-info/METADATA,sha256=9Y0V7Up2efXCRtKZ7Cceawz9LHvNcfH0olmEGK2mVk0,16326
|
|
30
|
+
wxpath-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
31
|
+
wxpath-0.3.0.dist-info/entry_points.txt,sha256=FwoIOnUTl-DjPqVw-eb9EHHiiXCyRZy_mEQKFu2eb5Y,43
|
|
32
|
+
wxpath-0.3.0.dist-info/top_level.txt,sha256=uFCcveG78mnefxRGvYsR2OexDlKR_Z1UD4vZijUcex8,7
|
|
33
|
+
wxpath-0.3.0.dist-info/RECORD,,
|
wxpath/core/errors.py
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import collections.abc as cabc
|
|
3
|
-
import functools
|
|
4
|
-
import inspect
|
|
5
|
-
import types
|
|
6
|
-
from contextlib import contextmanager
|
|
7
|
-
from contextvars import ContextVar
|
|
8
|
-
from enum import Enum, auto
|
|
9
|
-
from typing import AsyncGenerator
|
|
10
|
-
|
|
11
|
-
from wxpath.util.logging import get_logger
|
|
12
|
-
|
|
13
|
-
log = get_logger(__name__)
|
|
14
|
-
|
|
15
|
-
class ErrorPolicy(Enum):
|
|
16
|
-
IGNORE = auto() # swallow completely
|
|
17
|
-
LOG = auto() # just log at ERROR
|
|
18
|
-
COLLECT = auto() # yield {"_error": ..., "_ctx": ...}
|
|
19
|
-
RAISE = auto() # re-raise
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
_GLOBAL_DEFAULT = ErrorPolicy.LOG
|
|
23
|
-
|
|
24
|
-
# Task-local override (None => fall back to _GLOBAL_DEFAULT)
|
|
25
|
-
_CURRENT: ContextVar[ErrorPolicy | None] = ContextVar("wx_err_policy", default=None)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_current_error_policy() -> ErrorPolicy:
|
|
29
|
-
return _CURRENT.get() or _GLOBAL_DEFAULT
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def set_default_error_policy(policy: ErrorPolicy) -> None:
|
|
33
|
-
global _GLOBAL_DEFAULT
|
|
34
|
-
_GLOBAL_DEFAULT = policy
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@contextmanager
|
|
38
|
-
def use_error_policy(policy: ErrorPolicy):
|
|
39
|
-
token = _CURRENT.set(policy)
|
|
40
|
-
try:
|
|
41
|
-
yield
|
|
42
|
-
finally:
|
|
43
|
-
_CURRENT.reset(token)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def handle_error(exc: Exception, policy: ErrorPolicy, ctx: dict):
|
|
47
|
-
if policy is ErrorPolicy.IGNORE:
|
|
48
|
-
return None
|
|
49
|
-
|
|
50
|
-
if policy is ErrorPolicy.LOG:
|
|
51
|
-
log.exception("processing error", extra=ctx)
|
|
52
|
-
return None
|
|
53
|
-
|
|
54
|
-
if policy is ErrorPolicy.COLLECT:
|
|
55
|
-
return {"_error": str(exc), "_ctx": ctx}
|
|
56
|
-
|
|
57
|
-
# RAISE (safe default)
|
|
58
|
-
raise exc
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _is_gen(obj): # helper
|
|
62
|
-
return isinstance(obj, (types.GeneratorType, cabc.Generator))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def with_errors():
|
|
66
|
-
"""
|
|
67
|
-
Apply the current ErrorPolicy at call time while preserving the callable kind:
|
|
68
|
-
- async generator -> async generator wrapper
|
|
69
|
-
- coroutine -> async wrapper
|
|
70
|
-
- sync generator -> sync generator wrapper
|
|
71
|
-
- plain function -> plain wrapper
|
|
72
|
-
"""
|
|
73
|
-
def decorator(fn):
|
|
74
|
-
# --- ASYNC GENERATOR ---
|
|
75
|
-
if inspect.isasyncgenfunction(fn):
|
|
76
|
-
@functools.wraps(fn)
|
|
77
|
-
async def asyncgen_wrapped(*a, **kw) -> AsyncGenerator:
|
|
78
|
-
try:
|
|
79
|
-
async for item in fn(*a, **kw):
|
|
80
|
-
yield item
|
|
81
|
-
except Exception as exc:
|
|
82
|
-
collected = handle_error(exc, get_current_error_policy(),
|
|
83
|
-
_ctx_from_sig(fn, a, kw))
|
|
84
|
-
if collected is not None:
|
|
85
|
-
yield collected
|
|
86
|
-
return asyncgen_wrapped
|
|
87
|
-
|
|
88
|
-
# --- COROUTINE ---
|
|
89
|
-
if inspect.iscoroutinefunction(fn):
|
|
90
|
-
@functools.wraps(fn)
|
|
91
|
-
async def coro_wrapped(*a, **kw):
|
|
92
|
-
try:
|
|
93
|
-
return await fn(*a, **kw)
|
|
94
|
-
except Exception as exc:
|
|
95
|
-
return handle_error(exc, get_current_error_policy(),
|
|
96
|
-
_ctx_from_sig(fn, a, kw))
|
|
97
|
-
return coro_wrapped
|
|
98
|
-
|
|
99
|
-
# --- SYNC GENERATOR ---
|
|
100
|
-
if inspect.isgeneratorfunction(fn):
|
|
101
|
-
@functools.wraps(fn)
|
|
102
|
-
def gen_wrapped(*a, **kw):
|
|
103
|
-
try:
|
|
104
|
-
for item in fn(*a, **kw):
|
|
105
|
-
yield item
|
|
106
|
-
except Exception as exc:
|
|
107
|
-
collected = handle_error(exc, get_current_error_policy(),
|
|
108
|
-
_ctx_from_sig(fn, a, kw))
|
|
109
|
-
if collected is not None:
|
|
110
|
-
yield collected
|
|
111
|
-
return gen_wrapped
|
|
112
|
-
|
|
113
|
-
# --- PLAIN SYNC FUNCTION ---
|
|
114
|
-
@functools.wraps(fn)
|
|
115
|
-
def plain_wrapped(*a, **kw):
|
|
116
|
-
try:
|
|
117
|
-
return fn(*a, **kw)
|
|
118
|
-
except Exception as exc:
|
|
119
|
-
return handle_error(exc, get_current_error_policy(),
|
|
120
|
-
_ctx_from_sig(fn, a, kw))
|
|
121
|
-
return plain_wrapped
|
|
122
|
-
return decorator
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _ctx_from_sig(fn, a, kw):
|
|
126
|
-
"""Best-effort extraction of depth/url/op for logging."""
|
|
127
|
-
# you already pass these in every handler, so pull by position
|
|
128
|
-
try:
|
|
129
|
-
elem, segs, depth, *_ = a
|
|
130
|
-
op, val = segs[0] if segs else ("?", "?")
|
|
131
|
-
url = getattr(elem, "base_url", None)
|
|
132
|
-
return {"op": op, "depth": depth, "url": url}
|
|
133
|
-
except Exception:
|
|
134
|
-
return {}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|