flru-parser 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.
@@ -0,0 +1,421 @@
1
+ Metadata-Version: 2.4
2
+ Name: flru-parser
3
+ Version: 0.3.0
4
+ Summary: Simple, typed and resilient async/sync parser client for public FL.ru pages
5
+ Author: Andrey
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://github.com/N3ffus/flru-parser#readme
8
+ Project-URL: Changelog, https://github.com/N3ffus/flru-parser/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/N3ffus/flru-parser/issues
10
+ Project-URL: Repository, https://github.com/N3ffus/flru-parser
11
+ Keywords: fl.ru,freelance,parser,scraper,asyncio,httpx
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: beautifulsoup4<5,>=4.12
27
+ Requires-Dist: httpx[http2]<1,>=0.28
28
+ Requires-Dist: lxml<7,>=5
29
+ Requires-Dist: pydantic<3,>=2.10
30
+ Provides-Extra: postgres
31
+ Requires-Dist: asyncpg<1,>=0.30; extra == "postgres"
32
+ Provides-Extra: redis
33
+ Requires-Dist: redis<8,>=5.2; extra == "redis"
34
+ Provides-Extra: observability
35
+ Requires-Dist: opentelemetry-api<2,>=1.29; extra == "observability"
36
+ Requires-Dist: opentelemetry-sdk<2,>=1.29; extra == "observability"
37
+ Requires-Dist: prometheus-client<1,>=0.21; extra == "observability"
38
+ Dynamic: license-file
39
+
40
+ # flru-parser
41
+
42
+ [![PyPI](https://img.shields.io/pypi/v/flru-parser.svg)](https://pypi.org/project/flru-parser/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/flru-parser.svg)](https://pypi.org/project/flru-parser/)
44
+ [![License](https://img.shields.io/pypi/l/flru-parser.svg)](LICENSE)
45
+ [![Typing](https://img.shields.io/badge/typing-PEP%20561-blue)](src/flru/py.typed)
46
+ [![Coverage](https://img.shields.io/badge/branch%20coverage-85.76%25-brightgreen)](#quality)
47
+ [![Tests](https://img.shields.io/badge/tests-47%20passed-brightgreen)](#quality)
48
+ [![CI](https://github.com/N3ffus/flru-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/N3ffus/flru-parser/actions/workflows/ci.yml)
49
+
50
+ A typed, resilient, read-only Python client for public pages on [FL.ru](https://www.fl.ru/).
51
+ The default API is intentionally small; production controls remain available when needed.
52
+
53
+ > **Unofficial project.** This package is not affiliated with or endorsed by FL.ru. FL.ru does not expose a stable public API for all supported data. HTML selectors may require maintenance after a site redesign. The parser detects likely selector drift instead of silently returning an empty catalog.
54
+
55
+ [Русская документация](docs/ru/README.md) · [Simple API](docs/SIMPLE_API.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)
56
+
57
+ ## Features
58
+
59
+ - Simple async API: `projects()`, `project()`, `user()`, `freelancers()`, `new_projects()`.
60
+ - Matching synchronous API in `flru.sync`.
61
+ - Projects, details, categories, freelancers, profiles, reviews and portfolios.
62
+ - Typed Pydantic models with PEP 561 support.
63
+ - Retries, jitter, `Retry-After`, shared HTTP 429 cooldown and retry budgets.
64
+ - RPS and concurrency limits, endpoint/proxy circuit breakers and proxy health tracking.
65
+ - HTTP, HTTPS and SOCKS proxy pools with credential redaction.
66
+ - Safe redirects, allowed-host enforcement, `robots.txt` support and block-page detection.
67
+ - Incremental crawling with Memory, SQLite, PostgreSQL or Redis state.
68
+ - Parse diagnostics, confidence, field provenance and page fingerprints.
69
+ - Prometheus, OpenTelemetry and structured event integrations.
70
+ - CI, branch-coverage gate, dependency audit, live canary and PyPI Trusted Publishing.
71
+
72
+ ## Installation
73
+
74
+ ```bash
75
+ uv add flru-parser
76
+ ```
77
+
78
+ Optional integrations:
79
+
80
+ ```bash
81
+ uv add "flru-parser[postgres]"
82
+ uv add "flru-parser[redis]"
83
+ uv add "flru-parser[observability]"
84
+ ```
85
+
86
+ Supported Python versions: **3.11–3.13**.
87
+
88
+ ## Five-minute API
89
+
90
+ ### Async
91
+
92
+ ```python
93
+ import asyncio
94
+
95
+ from flru import Client
96
+
97
+
98
+ async def main() -> None:
99
+ async with Client() as fl:
100
+ projects = await fl.projects(pages=5)
101
+
102
+ for project in projects:
103
+ print(project.title, project.budget_min, project.currency, project.url)
104
+
105
+
106
+ asyncio.run(main())
107
+ ```
108
+
109
+ ### Sync
110
+
111
+ ```python
112
+ from flru.sync import Client
113
+
114
+ with Client() as fl:
115
+ projects = fl.projects(pages=5)
116
+
117
+ for project in projects:
118
+ print(project.title, project.budget_min, project.url)
119
+ ```
120
+
121
+ ### One-shot helpers
122
+
123
+ ```python
124
+ import asyncio
125
+
126
+ from flru import fetch_project, fetch_projects
127
+
128
+ projects = asyncio.run(fetch_projects(pages=3, query="FastAPI"))
129
+ project = asyncio.run(fetch_project(projects[0].id))
130
+ ```
131
+
132
+ For synchronous scripts:
133
+
134
+ ```python
135
+ from flru.sync import project, projects
136
+
137
+ items = projects(pages=3, query="FastAPI")
138
+ detail = project(items[0].id)
139
+ ```
140
+
141
+ ## Common operations
142
+
143
+ ### Search and filter projects
144
+
145
+ No separate filter object is needed for the common path:
146
+
147
+ ```python
148
+ projects = await fl.projects(
149
+ pages=10,
150
+ query="Python FastAPI",
151
+ category="programmirovanie/python",
152
+ min_budget=30_000,
153
+ max_budget=200_000,
154
+ types="order", # order, vacancy, contest
155
+ with_budget=True,
156
+ concurrency=3,
157
+ )
158
+ ```
159
+
160
+ `types` accepts one value or a sequence. English and Russian aliases are supported:
161
+
162
+ ```python
163
+ projects = await fl.projects(types=["заказ", "вакансия"])
164
+ ```
165
+
166
+ ### Parse the complete catalog
167
+
168
+ ```python
169
+ projects = await fl.projects(pages="all")
170
+ ```
171
+
172
+ For a large catalog, stream results instead of retaining everything in memory:
173
+
174
+ ```python
175
+ async for project in fl.stream_projects(pages="all", concurrency=3):
176
+ print(project.id, project.title)
177
+ ```
178
+
179
+ ### Load full project cards
180
+
181
+ ```python
182
+ details = await fl.projects(pages=3, details=True)
183
+
184
+ for project in details:
185
+ print(project.full_description, project.attachments)
186
+ ```
187
+
188
+ Or retrieve one project by ID or URL:
189
+
190
+ ```python
191
+ project = await fl.project(5500001)
192
+ project = await fl.project("https://www.fl.ru/projects/5500001/example.html")
193
+ ```
194
+
195
+ ### Profiles and freelancers
196
+
197
+ ```python
198
+ profile = await fl.user("username")
199
+ full_profile = await fl.user("username", full=True, pages=3)
200
+ freelancers = await fl.freelancers(pages=5, category="programmirovanie")
201
+ ```
202
+
203
+ `full=True` concurrently loads projects, reviews and portfolio sections.
204
+
205
+ ### Incremental crawling
206
+
207
+ The simplest durable mode uses a local SQLite file automatically:
208
+
209
+ ```python
210
+ new_or_changed = await fl.new_projects(
211
+ "flru-state.db",
212
+ pages=30,
213
+ stop_after_known=20,
214
+ )
215
+ ```
216
+
217
+ The state tracks content hashes, `first_seen_at`, `last_seen_at` and crawl checkpoints. Repeated runs stop after enough already-known records.
218
+
219
+ Advanced stores remain available:
220
+
221
+ ```python
222
+ from flru import PostgresStateStore
223
+
224
+ state = PostgresStateStore("postgresql://user:pass@localhost/app")
225
+ projects = await fl.new_projects(state, pages="all")
226
+ await state.close()
227
+ ```
228
+
229
+ ### Proxies, cookies and request limits
230
+
231
+ Common production options are flat constructor arguments:
232
+
233
+ ```python
234
+ async with Client(
235
+ concurrency=3,
236
+ rps=0.8,
237
+ retries=6,
238
+ timeout=45,
239
+ proxy=[
240
+ "http://user:password@proxy-1.example:8080",
241
+ "socks5://user:password@proxy-2.example:1080",
242
+ ],
243
+ cookies="cookies.txt",
244
+ ) as fl:
245
+ projects = await fl.projects(pages=10)
246
+ ```
247
+
248
+ `cookies` can be a mapping or a Netscape-format browser export path.
249
+ Proxy credentials are redacted from metrics and events.
250
+
251
+ ### Models
252
+
253
+ Models are regular Pydantic models:
254
+
255
+ ```python
256
+ project = projects[0]
257
+
258
+ print(project.budget_min)
259
+ print(project.budget_max)
260
+ print(project.currency)
261
+ print(project.customer_username)
262
+ print(project.to_dict())
263
+ print(project.model_dump_json(indent=2))
264
+ ```
265
+
266
+ ## Advanced API
267
+
268
+ The simple `Client` subclasses the full `FLClient`, so low-level methods remain directly available:
269
+
270
+ ```python
271
+ from flru import FLClient, ProjectFilters
272
+
273
+ async with FLClient() as client:
274
+ page = await client.get_projects_page(page=1)
275
+ batch = await client.get_projects_batch_result(range(1, 11), concurrency=4)
276
+ categories = await client.get_categories()
277
+ ```
278
+
279
+ Use `FLClient` and the immutable configuration dataclasses when every transport setting must be controlled:
280
+
281
+ ```python
282
+ from flru import (
283
+ CircuitBreakerConfig,
284
+ ClientConfig,
285
+ FLClient,
286
+ ProxyConfig,
287
+ RateLimitConfig,
288
+ RetryConfig,
289
+ )
290
+
291
+ config = ClientConfig(
292
+ retry=RetryConfig(
293
+ max_attempts=6,
294
+ base_delay=1,
295
+ max_delay=45,
296
+ total_timeout=120,
297
+ max_total_delay=60,
298
+ ),
299
+ rate_limit=RateLimitConfig(
300
+ requests_per_second=0.8,
301
+ max_concurrency=4,
302
+ min_interval=0.4,
303
+ ),
304
+ circuit_breaker=CircuitBreakerConfig(
305
+ failure_threshold=8,
306
+ recovery_timeout=90,
307
+ scope="endpoint_proxy",
308
+ ),
309
+ proxies=ProxyConfig(
310
+ urls=("http://user:password@proxy.example:8080",),
311
+ direct_fallback=True,
312
+ ),
313
+ )
314
+
315
+ client = FLClient(config)
316
+ ```
317
+
318
+ See [docs/SIMPLE_API.md](docs/SIMPLE_API.md) for the complete high-level API and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for internals.
319
+
320
+ ## Reliability behavior
321
+
322
+ - Empty output is not automatically treated as the end of the catalog.
323
+ - Candidate links with no parsed cards raise `SelectorDriftError`.
324
+ - Unclassified empty pages raise `EmptyPageError`.
325
+ - A recognized end page contains the `catalog_end` diagnostic warning.
326
+ - HTTP 429 can pause all concurrent workers through a shared cooldown.
327
+ - Circuit breakers are scoped by endpoint and proxy by default.
328
+ - CAPTCHA and block pages are detected, not bypassed.
329
+ - Requests are restricted to allowed FL.ru hosts and safe redirects.
330
+
331
+ ## Observability
332
+
333
+ ```python
334
+ metrics = await fl.stats()
335
+ print(metrics.requests_total, metrics.retries_total, metrics.endpoints)
336
+ ```
337
+
338
+ Structured logging:
339
+
340
+ ```python
341
+ from flru import Client, StructuredLogHandler
342
+
343
+ fl = Client(event_handler=StructuredLogHandler())
344
+ ```
345
+
346
+ Optional adapters:
347
+
348
+ ```python
349
+ from flru.integrations.opentelemetry import OpenTelemetryEventHandler
350
+ from flru.integrations.prometheus import PrometheusEventHandler
351
+ ```
352
+
353
+ Install `flru-parser[observability]` before using these adapters.
354
+
355
+ ## Exceptions
356
+
357
+ | Exception | Meaning |
358
+ |---|---|
359
+ | `BlockedError` | CAPTCHA, anti-bot or access-block page detected |
360
+ | `AuthenticationRequired` | Authenticated cookies are required |
361
+ | `RateLimitedError` | HTTP 429 after retry policy exhaustion |
362
+ | `CircuitOpenError` | A scoped circuit breaker is open |
363
+ | `SecurityError` | Host, scheme or redirect violates policy |
364
+ | `RobotsDeniedError` | `robots.txt` denies access |
365
+ | `SelectorDriftError` | Candidate records exist but current selectors parsed none |
366
+ | `EmptyPageError` | Empty page cannot be classified as catalog end |
367
+ | `ParseError` | HTML cannot be converted into the requested model |
368
+
369
+ ## Quality
370
+
371
+ Current verified result for **0.3.0**:
372
+
373
+ ```text
374
+ 47 passed, 1 optional Hypothesis suite skipped locally
375
+ Branch coverage: 85.76%
376
+ Coverage gate: 85%
377
+ ```
378
+
379
+ Run the complete local checks:
380
+
381
+ ```bash
382
+ uv sync --group dev
383
+ uv run ruff check .
384
+ uv run ruff format --check .
385
+ uv run mypy src/flru
386
+ uv run pytest --cov=flru --cov-branch --cov-report=term-missing --cov-report=html
387
+ uv build
388
+ uvx twine check dist/*
389
+ ```
390
+
391
+ The repository also contains:
392
+
393
+ - Python 3.11–3.13 CI matrix;
394
+ - scheduled dependency vulnerability audit;
395
+ - read-only live selector canary;
396
+ - pre-commit configuration;
397
+ - Trusted Publishing workflows for TestPyPI and PyPI.
398
+
399
+ ## Publishing
400
+
401
+ The release workflow uses PyPI Trusted Publishing and checks that the Git tag matches both package version declarations.
402
+
403
+ ```bash
404
+ uv run python scripts/configure_project.py YOUR_GITHUB_USERNAME
405
+ git tag -a v0.3.0 -m "flru-parser 0.3.0"
406
+ git push origin main v0.3.0
407
+ ```
408
+
409
+ See [docs/PUBLISHING.md](docs/PUBLISHING.md) for the one-time PyPI and GitHub setup.
410
+
411
+ ## Responsible use
412
+
413
+ - Respect FL.ru terms, `robots.txt`, applicable law and personal-data requirements.
414
+ - Prefer incremental crawling and reasonable request rates.
415
+ - Do not use proxies or this package to bypass access controls or CAPTCHA.
416
+ - Do not automate account actions through undocumented endpoints.
417
+ - Public indexability does not automatically grant unlimited collection or redistribution rights.
418
+
419
+ ## License
420
+
421
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,33 @@
1
+ flru/__init__.py,sha256=lFrVs-PRWp67zd5EOLaJN2yxFfl-2g8m-lkg3gmhQ9I,2704
2
+ flru/batch.py,sha256=kYDgVJJf-1gy64T5PxPqQYIB0q2fJDz8UbJ3RsQomjk,625
3
+ flru/canary.py,sha256=ENeuztMB4G6P355nBUoLyUAy3o84lwyzD1nfj64AmZM,914
4
+ flru/client.py,sha256=fE_xCjB61nT7sXd9uY4MKKGipP6xi5_CkpaoeQ0Vb_4,30376
5
+ flru/config.py,sha256=-xLydcgEY-z7lMCZTvRcsW8kKaFBIFifUzgPyuf6ZE0,4226
6
+ flru/cookies.py,sha256=pov7XZMZyEJGmlMiRCfB7GyAoDiEGxS2tf7LAeEJBKo,500
7
+ flru/easy.py,sha256=6vKsYCVlPOH8Y_NJcn9joyTJPnHLj5k_qmQWsxppPeA,16990
8
+ flru/exceptions.py,sha256=aWzFv4lHIZXMoAH_jl7Vt1Y-KpEGh6QPZyQVTMz57Og,1598
9
+ flru/filters.py,sha256=07Zm8XCQP31B3IRl3Wf8L4aEGc8IQ4WYQVvQfpxp_8Q,1251
10
+ flru/models.py,sha256=eEO9KXEhSdpdVirf28Pe1N9ZucZR-hnlGujp-T-DFyg,8474
11
+ flru/observability.py,sha256=NyWZuJPEs8gor9Uln2JjxHG77QPqWhi3RSxwJn3mxkM,3960
12
+ flru/proxy.py,sha256=lklKtZu_GO35VPOz-ocPWgrRkSw39MzoXaEn2R7z9M4,2878
13
+ flru/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ flru/resilience.py,sha256=a5WH6ljiFzjKAME5j4PNIf5pJ_oWRl3JahIpPWuatTk,5387
15
+ flru/robots.py,sha256=CTQPe4BCm-SxUHZt73fcqWTmmwNulBj2ox3ccIpyO08,1973
16
+ flru/security.py,sha256=WFBF98hnlefSwlLnLuhXSSEEw5vlJlMw9MNi40BUHRM,1455
17
+ flru/state.py,sha256=8T337jecGKScKv_IU4Sf1S4-0gQgsLlroa1hNUFBotA,12383
18
+ flru/sync.py,sha256=u3rJt2cbnnEWtdfHMm0J2Kef1cmU8JgcsexDme7XOJE,6710
19
+ flru/transport.py,sha256=SBtN2ZP1NwXgdItOgiBbrEbN8CwBArlJVzrzuyyaB9k,14025
20
+ flru/integrations/__init__.py,sha256=cXG129B1iZSQ3O39LJART1PFK_3p6abMMrxoQEJeCmY,43
21
+ flru/integrations/opentelemetry.py,sha256=0-WGJPFnuON5Te21ZFNa1TIVdHu974MgK2KsFp6aKHE,990
22
+ flru/integrations/prometheus.py,sha256=JYRoM2NFYcDWQzGQ0RHqLdhGpEE7pBGe8pn1EoFBnkQ,1198
23
+ flru/parsers/__init__.py,sha256=lEFj6Be5gHHvaCtWQFH0BiRoVzyGnVoOQczCNTwfvwQ,365
24
+ flru/parsers/common.py,sha256=_Uef1_yPorBSr-9NXoGaIYmaIYnsopoKz-N1ovkF1NQ,9270
25
+ flru/parsers/freelancers.py,sha256=Ukq2UwZGQiknuKNGEexULTROi4Hi-agvDHAatYvwIbQ,4936
26
+ flru/parsers/projects.py,sha256=PwmkU2RHF1Ow28Xfo4tTjkKOFb-_RWNOPPGdLGCA9CI,13414
27
+ flru/parsers/users.py,sha256=HRBftGXW3Ju1TKZZJtQJ833_eRkg0Jnu0pMcGN-8JXE,7380
28
+ flru_parser-0.3.0.dist-info/licenses/LICENSE,sha256=1zNNV91mmCQYfzAOrjvPh0c18NR6BImrUBS18w3yvFc,1063
29
+ flru_parser-0.3.0.dist-info/METADATA,sha256=3m4FQqjRQ9dKHn7HWAXN6Uic5jHyzjICdXnkLQ_jZa8,12430
30
+ flru_parser-0.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
31
+ flru_parser-0.3.0.dist-info/entry_points.txt,sha256=pA1nzHVJnt6IjNusyCW8paiu4oUbRfTwzp669owuEAY,49
32
+ flru_parser-0.3.0.dist-info/top_level.txt,sha256=1oNJlhBZ1TyECwrj5InG0Kfi1P_KeEwJO2ASSBD-9uw,5
33
+ flru_parser-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flru-canary = flru.canary:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ flru