snowloader 0.2.0__tar.gz → 0.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.
- {snowloader-0.2.0 → snowloader-0.2.2}/CHANGELOG.md +13 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/PKG-INFO +3 -3
- {snowloader-0.2.0 → snowloader-0.2.2}/README.md +2 -2
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/conf.py +1 -1
- {snowloader-0.2.0 → snowloader-0.2.2}/pyproject.toml +1 -1
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/__init__.py +1 -1
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/async_connection.py +18 -3
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_async_connection.py +57 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/.github/workflows/ci.yml +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/.github/workflows/docs.yml +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/.github/workflows/publish.yml +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/.gitignore +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/.readthedocs.yaml +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/LICENSE +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/_static/logo.png +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/adapters.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/advanced.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/api.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/async.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/attachments.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/authentication.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/changelog.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/configuration.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/getting-started.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/index.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/loaders.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/docs/roadmap.rst +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/examples/01_basic_incidents.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/examples/02_langchain_rag.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/examples/03_llamaindex_rag.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/examples/04_delta_sync.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/examples/05_cmdb_graph.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/langchain-docs/snowloader.ipynb +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/.github/workflows/publish.yml +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/.gitignore +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/LICENSE +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/Makefile +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/README.md +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/llama_index/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/llama_index/readers/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/llama_index/readers/snowloader/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/llama_index/readers/snowloader/base.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/pyproject.toml +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/tests/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/tests/test_readers_snowloader.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/adapters/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/adapters/langchain.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/adapters/llamaindex.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/async_models.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/connection.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/_field_utils.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/attachments.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/catalog.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/changes.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/cmdb.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/incidents.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/knowledge_base.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/problems.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/models.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/py.typed +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/utils/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/utils/html_cleaner.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/utils/parsing.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/integration/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/integration/test_live.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/__init__.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_async_loaders.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_attachments.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_catalog.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_changes.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_cmdb.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_connection.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_field_utils.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_html_cleaner.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_incidents.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_kb.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_langchain_adapter.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_llamaindex_adapter.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_models.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_problems.py +0 -0
- {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_smoke_e2e.py +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to snowloader are documented here. This project follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [0.2.2] - 2026-04-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `AsyncSnowConnection.aget_records` no longer crashes with `AttributeError: 'NoneType' object has no attribute 'get'` when the API returns a non-object JSON body (e.g. `null` or a list under transient load). Such responses are now logged and treated as an empty result, letting the rest of the pages stream through.
|
|
10
|
+
- `AsyncSnowConnection._request` now validates that the parsed JSON body is a dict before returning, so downstream callers can rely on the `dict[str, Any]` contract.
|
|
11
|
+
|
|
12
|
+
## [0.2.1] - 2026-04-28
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Roadmap entries for Async support and the Attachment loader marked as Shipped.
|
|
17
|
+
|
|
5
18
|
## [0.2.0] - 2026-04-28
|
|
6
19
|
|
|
7
20
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snowloader
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Comprehensive ServiceNow data loader for AI/LLM pipelines - Incidents, CMDB, KB, Changes, Catalog & more. Works with LangChain & LlamaIndex.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ronidas39/snowloader
|
|
6
6
|
Project-URL: Documentation, https://snowloader.readthedocs.io
|
|
@@ -308,8 +308,8 @@ See the [full documentation](https://snowloader.readthedocs.io/en/latest/configu
|
|
|
308
308
|
|
|
309
309
|
| Version | Feature | Status |
|
|
310
310
|
|---------|---------|--------|
|
|
311
|
-
| **v0.2** | Async support (`aiohttp` + `async for`) - 10-50x faster |
|
|
312
|
-
| **v0.2** | Attachment loader (`sys_attachment` downloads) |
|
|
311
|
+
| **v0.2** | Async support (`aiohttp` + `async for`) - 10-50x faster | Shipped |
|
|
312
|
+
| **v0.2** | Attachment loader (`sys_attachment` downloads) | Shipped |
|
|
313
313
|
| **v0.3** | Direct vector store streaming (Pinecone, Weaviate, Chroma) | Planned |
|
|
314
314
|
| **v0.3** | Checkpoint and resume for large loads | Planned |
|
|
315
315
|
| **v1.0** | Custom field mapping for customized instances | Planned |
|
|
@@ -252,8 +252,8 @@ See the [full documentation](https://snowloader.readthedocs.io/en/latest/configu
|
|
|
252
252
|
|
|
253
253
|
| Version | Feature | Status |
|
|
254
254
|
|---------|---------|--------|
|
|
255
|
-
| **v0.2** | Async support (`aiohttp` + `async for`) - 10-50x faster |
|
|
256
|
-
| **v0.2** | Attachment loader (`sys_attachment` downloads) |
|
|
255
|
+
| **v0.2** | Async support (`aiohttp` + `async for`) - 10-50x faster | Shipped |
|
|
256
|
+
| **v0.2** | Attachment loader (`sys_attachment` downloads) | Shipped |
|
|
257
257
|
| **v0.3** | Direct vector store streaming (Pinecone, Weaviate, Chroma) | Planned |
|
|
258
258
|
| **v0.3** | Checkpoint and resume for large loads | Planned |
|
|
259
259
|
| **v1.0** | Custom field mapping for customized instances | Planned |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "snowloader"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Comprehensive ServiceNow data loader for AI/LLM pipelines - Incidents, CMDB, KB, Changes, Catalog & more. Works with LangChain & LlamaIndex."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -21,7 +21,7 @@ from snowloader.loaders.problems import ProblemLoader
|
|
|
21
21
|
from snowloader.models import BaseSnowLoader, SnowDocument
|
|
22
22
|
from snowloader.utils.parsing import parse_labelled_int
|
|
23
23
|
|
|
24
|
-
__version__ = "0.2.
|
|
24
|
+
__version__ = "0.2.2"
|
|
25
25
|
|
|
26
26
|
try:
|
|
27
27
|
from snowloader.async_connection import AsyncSnowConnection # noqa: F401
|
|
@@ -376,13 +376,28 @@ class AsyncSnowConnection:
|
|
|
376
376
|
)
|
|
377
377
|
|
|
378
378
|
try:
|
|
379
|
-
|
|
379
|
+
parsed = await resp.json(content_type=None)
|
|
380
380
|
except (aiohttp.ContentTypeError, ValueError) as exc:
|
|
381
381
|
raise SnowConnectionError(
|
|
382
382
|
f"API returned non-JSON response for {method} {url}",
|
|
383
383
|
status_code=resp.status,
|
|
384
384
|
detail=last_body,
|
|
385
385
|
) from exc
|
|
386
|
+
|
|
387
|
+
if not isinstance(parsed, dict):
|
|
388
|
+
# Some ServiceNow paths can return ``null`` or a list
|
|
389
|
+
# under transient load. Treat anything that is not a
|
|
390
|
+
# JSON object as an empty result so downstream code
|
|
391
|
+
# does not crash on ``data.get(...)``.
|
|
392
|
+
logger.warning(
|
|
393
|
+
"API returned non-object JSON for %s %s (type=%s); "
|
|
394
|
+
"treating as empty result.",
|
|
395
|
+
method,
|
|
396
|
+
url,
|
|
397
|
+
type(parsed).__name__,
|
|
398
|
+
)
|
|
399
|
+
return {"result": []}
|
|
400
|
+
return cast(dict[str, Any], parsed)
|
|
386
401
|
except aiohttp.ClientError as exc:
|
|
387
402
|
if attempt < self.max_retries:
|
|
388
403
|
logger.warning(
|
|
@@ -489,8 +504,8 @@ class AsyncSnowConnection:
|
|
|
489
504
|
page_params["sysparm_offset"] = str(offset)
|
|
490
505
|
url = f"{self.instance_url}/api/now/table/{table}"
|
|
491
506
|
data = await self._request("GET", url, params=page_params)
|
|
492
|
-
raw = data.get("result")
|
|
493
|
-
if raw is None:
|
|
507
|
+
raw = data.get("result") if isinstance(data, dict) else None
|
|
508
|
+
if raw is None or not isinstance(raw, list):
|
|
494
509
|
return []
|
|
495
510
|
return cast(list[dict[str, object]], raw)
|
|
496
511
|
|
|
@@ -254,6 +254,63 @@ async def test_aget_attachment_returns_bytes() -> None:
|
|
|
254
254
|
assert data == b"binary file content"
|
|
255
255
|
|
|
256
256
|
|
|
257
|
+
@pytest.mark.asyncio
|
|
258
|
+
async def test_request_treats_null_body_as_empty_result() -> None:
|
|
259
|
+
"""Regression: ServiceNow can return JSON null under transient load.
|
|
260
|
+
|
|
261
|
+
Previously this crashed aget_records on ``data.get('result')``. Now we
|
|
262
|
+
log a warning and treat it as an empty result.
|
|
263
|
+
"""
|
|
264
|
+
with aioresponses() as m:
|
|
265
|
+
m.get(
|
|
266
|
+
_stats_url(),
|
|
267
|
+
payload={"result": {"stats": {"count": "1"}}},
|
|
268
|
+
status=200,
|
|
269
|
+
repeat=True,
|
|
270
|
+
)
|
|
271
|
+
m.get(
|
|
272
|
+
_table_url(),
|
|
273
|
+
body="null",
|
|
274
|
+
status=200,
|
|
275
|
+
content_type="application/json",
|
|
276
|
+
repeat=True,
|
|
277
|
+
)
|
|
278
|
+
async with AsyncSnowConnection(
|
|
279
|
+
instance_url=INSTANCE,
|
|
280
|
+
username="u",
|
|
281
|
+
password="p",
|
|
282
|
+
page_size=10,
|
|
283
|
+
) as conn:
|
|
284
|
+
records = [rec async for rec in conn.aget_records("incident")]
|
|
285
|
+
assert records == []
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@pytest.mark.asyncio
|
|
289
|
+
async def test_request_treats_list_body_as_empty_result() -> None:
|
|
290
|
+
"""Regression: defensive handling for unexpected non-object JSON shapes."""
|
|
291
|
+
with aioresponses() as m:
|
|
292
|
+
m.get(
|
|
293
|
+
_stats_url(),
|
|
294
|
+
payload={"result": {"stats": {"count": "1"}}},
|
|
295
|
+
status=200,
|
|
296
|
+
repeat=True,
|
|
297
|
+
)
|
|
298
|
+
m.get(
|
|
299
|
+
_table_url(),
|
|
300
|
+
payload=[1, 2, 3],
|
|
301
|
+
status=200,
|
|
302
|
+
repeat=True,
|
|
303
|
+
)
|
|
304
|
+
async with AsyncSnowConnection(
|
|
305
|
+
instance_url=INSTANCE,
|
|
306
|
+
username="u",
|
|
307
|
+
password="p",
|
|
308
|
+
page_size=10,
|
|
309
|
+
) as conn:
|
|
310
|
+
records = [rec async for rec in conn.aget_records("incident")]
|
|
311
|
+
assert records == []
|
|
312
|
+
|
|
313
|
+
|
|
257
314
|
@pytest.mark.asyncio
|
|
258
315
|
async def test_aget_attachment_raises_on_404() -> None:
|
|
259
316
|
with aioresponses() as m:
|
|
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
|
{snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/.gitignore
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/pyproject.toml
RENAMED
|
File without changes
|
{snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/tests/__init__.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|