core-lens 0.1.dev200__tar.gz → 0.1.dev202__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 (94) hide show
  1. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/PKG-INFO +1 -1
  2. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/_version.py +2 -2
  3. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/base/view.py +26 -4
  4. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_view.py +70 -0
  5. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  6. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  7. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  8. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.github/pull_request_template.md +0 -0
  9. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.github/workflows/ci.yml +0 -0
  10. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.github/workflows/gh-pages.yml +0 -0
  11. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.github/workflows/pre-release.yml +0 -0
  12. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.github/workflows/release.yml +0 -0
  13. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.gitignore +0 -0
  14. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.gitmessage +0 -0
  15. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.pre-commit-config.yaml +0 -0
  16. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/.python-version +0 -0
  17. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/CONTRIBUTING.md +0 -0
  18. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/LICENSE +0 -0
  19. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/README.md +0 -0
  20. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/SKILLS.md +0 -0
  21. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/README.md +0 -0
  22. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/bench_aoi.py +0 -0
  23. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/bench_entity.py +0 -0
  24. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/bench_export.py +0 -0
  25. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/bench_polars_utils.py +0 -0
  26. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/bench_result.py +0 -0
  27. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/bench_schema.py +0 -0
  28. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/bench_season.py +0 -0
  29. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/bench_spatial.py +0 -0
  30. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/bench_view.py +0 -0
  31. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/benchmarks/run_all.sh +0 -0
  32. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/Makefile +0 -0
  33. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/make.bat +0 -0
  34. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/concepts.md +0 -0
  35. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/conf.py +0 -0
  36. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/export.md +0 -0
  37. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/index.rst +0 -0
  38. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/intro.md +0 -0
  39. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/logging.md +0 -0
  40. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/plots.md +0 -0
  41. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/plugins.md +0 -0
  42. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/queries.md +0 -0
  43. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/quickstart.md +0 -0
  44. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/docs/source/stats.md +0 -0
  45. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/examples/demo_mws.py +0 -0
  46. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/examples/demo_tehsil.py +0 -0
  47. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/hooks/mypy.sh +0 -0
  48. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/hooks/no-parquet-outside-fixtures.sh +0 -0
  49. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/hooks/pytest.sh +0 -0
  50. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/pyproject.toml +0 -0
  51. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/__init__.py +0 -0
  52. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/__main__.py +0 -0
  53. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/aoi.py +0 -0
  54. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/base/__init__.py +0 -0
  55. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/base/entity.py +0 -0
  56. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/base/namespaces/__init__.py +0 -0
  57. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/base/namespaces/plot.py +0 -0
  58. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/base/namespaces/stats.py +0 -0
  59. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/base/result.py +0 -0
  60. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/entities/__init__.py +0 -0
  61. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/entities/farms.py +0 -0
  62. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/entities/mws.py +0 -0
  63. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/entities/tehsil.py +0 -0
  64. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/entities/waterbody.py +0 -0
  65. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/export/__init__.py +0 -0
  66. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/export/formats.py +0 -0
  67. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/py.typed +0 -0
  68. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/schema/__init__.py +0 -0
  69. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/schema/detection.py +0 -0
  70. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/schema/profile.py +0 -0
  71. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/utils/__init__.py +0 -0
  72. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/utils/paths.py +0 -0
  73. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/utils/polars_utils.py +0 -0
  74. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/utils/season.py +0 -0
  75. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/src/core_lens/utils/spatial.py +0 -0
  76. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/fixtures/generate_fixtures.py +0 -0
  77. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/conftest.py +0 -0
  78. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_aoi.py +0 -0
  79. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_entities.py +0 -0
  80. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_entity.py +0 -0
  81. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_export.py +0 -0
  82. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_main.py +0 -0
  83. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_plot.py +0 -0
  84. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_polars_utils.py +0 -0
  85. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_profile.py +0 -0
  86. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_result.py +0 -0
  87. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_schema_detection.py +0 -0
  88. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_schema_profile.py +0 -0
  89. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_season.py +0 -0
  90. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_season_config.py +0 -0
  91. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_spatial.py +0 -0
  92. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/tests/unit/test_stats.py +0 -0
  93. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/usage.md +0 -0
  94. {core_lens-0.1.dev200 → core_lens-0.1.dev202}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: core-lens
3
- Version: 0.1.dev200
3
+ Version: 0.1.dev202
4
4
  Summary: Query, analyse, and visualise CoreStack's microwatershed and Earth science data through a clean, composable Python API.
5
5
  Project-URL: Homepage, https://github.com/ApoorvaKashyap/core-lens
6
6
  Project-URL: Issues, https://github.com/ApoorvaKashyap/core-lens/issues
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.dev200'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev200')
21
+ __version__ = version = '0.1.dev202'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev202')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -234,15 +234,25 @@ class View:
234
234
  season_config=self._season_config,
235
235
  )
236
236
 
237
- def spatial_join(self, other: "BaseEntity", agg: dict[str, str]) -> "View":
237
+ def spatial_join(self, other: "BaseEntity | View", agg: dict[str, str]) -> "View":
238
238
  """Return a new View with a cross-entity join pending.
239
239
 
240
240
  The join is recorded in the View's ``join_spec`` and computed only at
241
241
  materialisation time (``.static``, ``.annual``, or ``.sub_annual``).
242
242
 
243
+ ``other`` may be either a :class:`~core_lens.base.entity.BaseEntity`
244
+ **or** a :class:`View` (e.g. ``aoi.waterbody``). When a ``View`` is
245
+ supplied its underlying ``.entity`` is extracted automatically, so
246
+ both of the following are equivalent::
247
+
248
+ aoi.mws.spatial_join(aoi.waterbody, agg={"kharif": "mean"})
249
+ aoi.mws.spatial_join(aoi.waterbody.entity, agg={"kharif": "mean"})
250
+
243
251
  Args:
244
- other (BaseEntity): The secondary :class:`BaseEntity` whose columns will be
245
- joined and aggregated onto ``self``.
252
+ other (BaseEntity | View): The secondary entity whose columns will
253
+ be joined and aggregated onto ``self``. Accepts either a
254
+ :class:`~core_lens.base.entity.BaseEntity` instance or a
255
+ :class:`View` (the latter is automatically unwrapped).
246
256
  agg (dict[str, str]): Mapping of ``{column: aggregation}`` specifying which columns
247
257
  from ``other`` to bring in and how to aggregate them.
248
258
 
@@ -256,12 +266,17 @@ class View:
256
266
  )
257
267
  raise ValueError("View already has a pending spatial_join.")
258
268
 
269
+ # Accept a View as `other` (e.g. aoi.waterbody) by unwrapping it.
270
+ # AoI attributes return scoped Views, not bare BaseEntity instances,
271
+ # so this is the natural way to reference a secondary entity.
272
+ other_entity = other.entity if isinstance(other, View) else other
273
+
259
274
  return View(
260
275
  keys=self.keys,
261
276
  entity=self.entity,
262
277
  entity_name=self.entity_name,
263
278
  time_filter=self.time_filter,
264
- join_spec={"other": other, "agg": agg},
279
+ join_spec={"other": other_entity, "agg": agg},
265
280
  season_config=self._season_config,
266
281
  )
267
282
 
@@ -513,6 +528,13 @@ class View:
513
528
  other = self.join_spec["other"]
514
529
  agg = self.join_spec["agg"]
515
530
 
531
+ # Defensively unwrap a View to its entity — join_spec["other"] may be a
532
+ # View when callers pass e.g. aoi.waterbody directly (AoI.__getattr__
533
+ # returns Views, not bare entities). spatial_join() already does this
534
+ # unwrap, but _materialise guards here too for robustness.
535
+ if isinstance(other, View):
536
+ other = other.entity
537
+
516
538
  from core_lens.base.entity import _entity_name as _ename
517
539
 
518
540
  other_entity_name = _ename(type(other))
@@ -297,6 +297,76 @@ class TestViewMaterialisation:
297
297
  with pytest.raises(ValueError, match="already has a pending spatial_join"):
298
298
  view.spatial_join(entity_cls(), {"district": "count"})
299
299
 
300
+ def test_spatial_join_accepts_view_as_other(self, entity_cls: Any) -> None:
301
+ """Passing a View as `other` (e.g. aoi.waterbody) should unwrap to the
302
+ underlying entity so that materialisation can call .schema_profile on it.
303
+
304
+ Regression test for: AttributeError: 'View' object has no attribute
305
+ 'schema_profile' raised inside execute_spatial_join when AoI attributes
306
+ (which return Views) were passed directly to spatial_join.
307
+ """
308
+ entity = entity_cls()
309
+ other_entity = entity_cls()
310
+ # Wrap the secondary entity in a View, mimicking what AoI.__getattr__ returns.
311
+ other_view = View(
312
+ keys=pl.DataFrame({"mws_id": []}, schema={"mws_id": pl.String}),
313
+ entity=other_entity,
314
+ entity_name="otherw",
315
+ )
316
+
317
+ view = View(
318
+ keys=pl.DataFrame({"mws_id": []}, schema={"mws_id": pl.String}),
319
+ entity=entity,
320
+ entity_name="minimalmws",
321
+ )
322
+ joined = view.spatial_join(other_view, {"district": "count"})
323
+
324
+ # The join_spec must store the unwrapped BaseEntity, not the View.
325
+ assert joined.join_spec is not None
326
+ assert joined.join_spec["other"] is other_entity
327
+ assert not isinstance(joined.join_spec["other"], View)
328
+
329
+ def test_spatial_join_view_other_materialises(self, entity_cls: Any) -> None:
330
+ """Materialising a View whose join `other` was supplied as a View should
331
+ produce the same prefixed columns as passing a bare entity directly.
332
+ """
333
+ import pathlib
334
+ import tempfile
335
+ import sys
336
+ import os
337
+
338
+ conftest_dir = os.path.dirname(__file__)
339
+ if conftest_dir not in sys.path:
340
+ sys.path.insert(0, conftest_dir)
341
+ from conftest import _make_static_parquet, _make_entity_cls
342
+
343
+ with tempfile.TemporaryDirectory() as td:
344
+ sp = pathlib.Path(td) / "static.parquet"
345
+ _make_static_parquet(sp)
346
+ full_cls = _make_entity_cls(sp)
347
+ full_entity = full_cls()
348
+ keys = full_entity._index.select(full_entity.key_cols)
349
+
350
+ # Build the secondary entity wrapped in a View — the pattern from aoi.X
351
+ other_view = View(
352
+ keys=keys,
353
+ entity=full_entity,
354
+ entity_name="minimalmws",
355
+ )
356
+
357
+ view = View(
358
+ keys=keys,
359
+ entity=full_entity,
360
+ entity_name="minimalmws",
361
+ join_spec={"other": other_view, "agg": {"district": "count"}},
362
+ )
363
+ result = view.static
364
+
365
+ joined_cols = [c for c in result.df().columns if c.startswith("minimalmws_")]
366
+ assert joined_cols, (
367
+ f"Expected prefixed join columns but got: {result.df().columns}"
368
+ )
369
+
300
370
  def test_spatial_join_fetches_geometry_if_missing(
301
371
  self, entity_cls_full: Any
302
372
  ) -> None:
File without changes
File without changes
File without changes
File without changes
File without changes