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.
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wxpath
3
- Version: 0.2.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.9
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
- # wxpath - declarative web crawling with XPath
22
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](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** evaluates that expression concurrently, breadth-first-*ish*, and streams results as they are discovered.
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, **wxpath**'s engine is able to perform deep, recursive web crawling and extraction.
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
- - [`url(...)` and `///url(...)` Explained](#url-and---explained)
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 support](#xpath-31-support)
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](#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
- path = """
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':(//div[contains(@class, 'shortdescription')]/text())[1]
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(path, max_depth=1):
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': TextNode('Computer language'), 'url': 'https://en.wikipedia.org/wiki/Computer_language', 'short_description': TextNode('Formal language for communicating with a computer')}
73
- map{'title': TextNode('Machine-readable medium and data'), 'url': 'https://en.wikipedia.org/wiki/Machine_readable', 'short_description': TextNode('Medium capable of storing data in a format readable by a machine')}
74
- map{'title': TextNode('Advanced Boolean Expression Language'), 'url': 'https://en.wikipedia.org/wiki/Advanced_Boolean_Expression_Language', 'short_description': TextNode('Hardware description language and software')}
75
- map{'title': TextNode('Jakarta Expression Language'), 'url': 'https://en.wikipedia.org/wiki/Jakarta_Expression_Language', 'short_description': TextNode('Computer programming language')}
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 infinite/recursive traversal. It tells **wxpath** to continue following links indefinitely, up to the specified `max_depth`. Unlike repeated `url()` hops, it allows a single expression to describe unbounded graph exploration. WARNING: Use with caution and constraints (via `max_depth` or XPath predicates) to avoid traversal explosion.
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, not per-depth and on a best-effort basis**.
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 infinite/recursive traversal - it proceeds breadth-first-*ish* up to `max_depth`.
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 supports concurrent requests using an asyncio-in-sync pattern, 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.
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, not just strings.
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
- WARNING: Due to the everchanging nature of web content, the output may vary over time.
205
+ NOTE: Due to the everchanging nature of web content, the output may vary over time.
192
206
  ```bash
193
- > wxpath --depth 1 "\
194
- url('https://en.wikipedia.org/wiki/Expression_language')\
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 per_host values, or with defaults.
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, recursive scraping without boilerplate
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
- ### Non-Goals/Limitations (for now)
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
- - Recursive (`///`) crawls require user discipline to avoid unbounded expansion (traversal explosion).
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&currency_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=CHOFWH_WHsJ30aItIQw9c5jzjl2Y64DmW2K942OGwpo,1668
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=8hc8VTqsxGFpizOyPTgzxjc8Y5srHd2aaOugQ9fJ3sE,8918
9
- wxpath/core/parser.py,sha256=0VQCkuznd4dYYzEeTAMFs1L2SmvTgSp1JWz-Um0uEjM,9911
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=Pn5wzPkBwp8bq48Ie0O0DVQzUFEAAzWIj1PHgChm2bo,10825
12
- wxpath/core/runtime/helpers.py,sha256=NCL4Wl8Hpc1VTfERSthCen9wlVd5J0eS8th4gqEPmRg,1578
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=q4MxYwDUv7LH4-WJGO_unXbBRFXXxsBCU4vU1co0gC4,4136
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=hN7EJXP102nsMA9ipaNPc9fWwDVpm_LJdGo6LSlAQp0,6996
20
- wxpath/http/client/request.py,sha256=3nwwPQ2e_WycJQnSA6QieWJ2q3qg40jkGrp2NUDPsLI,888
21
- wxpath/http/client/response.py,sha256=mDo3FswiVnulV1l5qjio5OQpGlT0-tfkR7daPSgSUuE,324
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.2.0.dist-info/licenses/LICENSE,sha256=AVBZLhdWmqxm-f-dy5prVB1E-solHWoP2EXEIV_o-00,1076
29
- wxpath-0.2.0.dist-info/METADATA,sha256=6CdIcq82gNqvXVIpBzhGCk_Q0eqDvok1JmEKWQkFals,14662
30
- wxpath-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- wxpath-0.2.0.dist-info/entry_points.txt,sha256=FwoIOnUTl-DjPqVw-eb9EHHiiXCyRZy_mEQKFu2eb5Y,43
32
- wxpath-0.2.0.dist-info/top_level.txt,sha256=uFCcveG78mnefxRGvYsR2OexDlKR_Z1UD4vZijUcex8,7
33
- wxpath-0.2.0.dist-info/RECORD,,
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