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.
Files changed (83) hide show
  1. {snowloader-0.2.0 → snowloader-0.2.2}/CHANGELOG.md +13 -0
  2. {snowloader-0.2.0 → snowloader-0.2.2}/PKG-INFO +3 -3
  3. {snowloader-0.2.0 → snowloader-0.2.2}/README.md +2 -2
  4. {snowloader-0.2.0 → snowloader-0.2.2}/docs/conf.py +1 -1
  5. {snowloader-0.2.0 → snowloader-0.2.2}/pyproject.toml +1 -1
  6. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/__init__.py +1 -1
  7. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/async_connection.py +18 -3
  8. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_async_connection.py +57 -0
  9. {snowloader-0.2.0 → snowloader-0.2.2}/.github/workflows/ci.yml +0 -0
  10. {snowloader-0.2.0 → snowloader-0.2.2}/.github/workflows/docs.yml +0 -0
  11. {snowloader-0.2.0 → snowloader-0.2.2}/.github/workflows/publish.yml +0 -0
  12. {snowloader-0.2.0 → snowloader-0.2.2}/.gitignore +0 -0
  13. {snowloader-0.2.0 → snowloader-0.2.2}/.readthedocs.yaml +0 -0
  14. {snowloader-0.2.0 → snowloader-0.2.2}/LICENSE +0 -0
  15. {snowloader-0.2.0 → snowloader-0.2.2}/docs/_static/logo.png +0 -0
  16. {snowloader-0.2.0 → snowloader-0.2.2}/docs/adapters.rst +0 -0
  17. {snowloader-0.2.0 → snowloader-0.2.2}/docs/advanced.rst +0 -0
  18. {snowloader-0.2.0 → snowloader-0.2.2}/docs/api.rst +0 -0
  19. {snowloader-0.2.0 → snowloader-0.2.2}/docs/async.rst +0 -0
  20. {snowloader-0.2.0 → snowloader-0.2.2}/docs/attachments.rst +0 -0
  21. {snowloader-0.2.0 → snowloader-0.2.2}/docs/authentication.rst +0 -0
  22. {snowloader-0.2.0 → snowloader-0.2.2}/docs/changelog.rst +0 -0
  23. {snowloader-0.2.0 → snowloader-0.2.2}/docs/configuration.rst +0 -0
  24. {snowloader-0.2.0 → snowloader-0.2.2}/docs/getting-started.rst +0 -0
  25. {snowloader-0.2.0 → snowloader-0.2.2}/docs/index.rst +0 -0
  26. {snowloader-0.2.0 → snowloader-0.2.2}/docs/loaders.rst +0 -0
  27. {snowloader-0.2.0 → snowloader-0.2.2}/docs/roadmap.rst +0 -0
  28. {snowloader-0.2.0 → snowloader-0.2.2}/examples/01_basic_incidents.py +0 -0
  29. {snowloader-0.2.0 → snowloader-0.2.2}/examples/02_langchain_rag.py +0 -0
  30. {snowloader-0.2.0 → snowloader-0.2.2}/examples/03_llamaindex_rag.py +0 -0
  31. {snowloader-0.2.0 → snowloader-0.2.2}/examples/04_delta_sync.py +0 -0
  32. {snowloader-0.2.0 → snowloader-0.2.2}/examples/05_cmdb_graph.py +0 -0
  33. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/langchain-docs/snowloader.ipynb +0 -0
  34. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/.github/workflows/publish.yml +0 -0
  35. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/.gitignore +0 -0
  36. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/LICENSE +0 -0
  37. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/Makefile +0 -0
  38. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/README.md +0 -0
  39. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/llama_index/__init__.py +0 -0
  40. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/llama_index/readers/__init__.py +0 -0
  41. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/llama_index/readers/snowloader/__init__.py +0 -0
  42. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/llama_index/readers/snowloader/base.py +0 -0
  43. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/pyproject.toml +0 -0
  44. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/tests/__init__.py +0 -0
  45. {snowloader-0.2.0 → snowloader-0.2.2}/integrations/llama-index-readers-snowloader/tests/test_readers_snowloader.py +0 -0
  46. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/adapters/__init__.py +0 -0
  47. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/adapters/langchain.py +0 -0
  48. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/adapters/llamaindex.py +0 -0
  49. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/async_models.py +0 -0
  50. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/connection.py +0 -0
  51. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/__init__.py +0 -0
  52. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/_field_utils.py +0 -0
  53. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/attachments.py +0 -0
  54. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/catalog.py +0 -0
  55. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/changes.py +0 -0
  56. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/cmdb.py +0 -0
  57. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/incidents.py +0 -0
  58. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/knowledge_base.py +0 -0
  59. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/loaders/problems.py +0 -0
  60. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/models.py +0 -0
  61. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/py.typed +0 -0
  62. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/utils/__init__.py +0 -0
  63. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/utils/html_cleaner.py +0 -0
  64. {snowloader-0.2.0 → snowloader-0.2.2}/src/snowloader/utils/parsing.py +0 -0
  65. {snowloader-0.2.0 → snowloader-0.2.2}/tests/__init__.py +0 -0
  66. {snowloader-0.2.0 → snowloader-0.2.2}/tests/integration/__init__.py +0 -0
  67. {snowloader-0.2.0 → snowloader-0.2.2}/tests/integration/test_live.py +0 -0
  68. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/__init__.py +0 -0
  69. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_async_loaders.py +0 -0
  70. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_attachments.py +0 -0
  71. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_catalog.py +0 -0
  72. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_changes.py +0 -0
  73. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_cmdb.py +0 -0
  74. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_connection.py +0 -0
  75. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_field_utils.py +0 -0
  76. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_html_cleaner.py +0 -0
  77. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_incidents.py +0 -0
  78. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_kb.py +0 -0
  79. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_langchain_adapter.py +0 -0
  80. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_llamaindex_adapter.py +0 -0
  81. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_models.py +0 -0
  82. {snowloader-0.2.0 → snowloader-0.2.2}/tests/unit/test_problems.py +0 -0
  83. {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.0
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 | Coming soon |
312
- | **v0.2** | Attachment loader (`sys_attachment` downloads) | Coming soon |
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 | Coming soon |
256
- | **v0.2** | Attachment loader (`sys_attachment` downloads) | Coming soon |
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 |
@@ -10,7 +10,7 @@ sys.path.insert(0, os.path.abspath("../src"))
10
10
  project = "snowloader"
11
11
  copyright = "2026, Roni Das"
12
12
  author = "Roni Das"
13
- release = "0.2.0"
13
+ release = "0.2.2"
14
14
 
15
15
  # -- General configuration ---------------------------------------------------
16
16
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "snowloader"
7
- version = "0.2.0"
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.0"
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
- return cast(dict[str, Any], await resp.json(content_type=None))
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