core-lens 0.1.dev132__tar.gz → 0.1.dev133__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 (91) hide show
  1. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/PKG-INFO +1 -1
  2. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/_version.py +2 -2
  3. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/base/entity.py +128 -15
  4. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  5. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  6. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  7. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.github/pull_request_template.md +0 -0
  8. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.github/workflows/ci.yml +0 -0
  9. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.github/workflows/gh-pages.yml +0 -0
  10. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.github/workflows/pre-release.yml +0 -0
  11. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.github/workflows/release.yml +0 -0
  12. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.gitignore +0 -0
  13. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.gitmessage +0 -0
  14. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.pre-commit-config.yaml +0 -0
  15. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/.python-version +0 -0
  16. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/CONTRIBUTING.md +0 -0
  17. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/LICENSE +0 -0
  18. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/README.md +0 -0
  19. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/SKILLS.md +0 -0
  20. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/README.md +0 -0
  21. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/bench_aoi.py +0 -0
  22. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/bench_export.py +0 -0
  23. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/bench_polars_utils.py +0 -0
  24. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/bench_result.py +0 -0
  25. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/bench_schema.py +0 -0
  26. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/bench_season.py +0 -0
  27. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/bench_spatial.py +0 -0
  28. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/bench_view.py +0 -0
  29. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/benchmarks/run_all.sh +0 -0
  30. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/Makefile +0 -0
  31. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/make.bat +0 -0
  32. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/concepts.md +0 -0
  33. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/conf.py +0 -0
  34. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/export.md +0 -0
  35. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/index.rst +0 -0
  36. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/intro.md +0 -0
  37. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/logging.md +0 -0
  38. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/plots.md +0 -0
  39. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/plugins.md +0 -0
  40. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/queries.md +0 -0
  41. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/quickstart.md +0 -0
  42. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/docs/source/stats.md +0 -0
  43. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/examples/demo_mws.py +0 -0
  44. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/examples/demo_tehsil.py +0 -0
  45. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/hooks/mypy.sh +0 -0
  46. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/hooks/no-parquet-outside-fixtures.sh +0 -0
  47. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/hooks/pytest.sh +0 -0
  48. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/pyproject.toml +0 -0
  49. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/__init__.py +0 -0
  50. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/__main__.py +0 -0
  51. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/aoi.py +0 -0
  52. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/base/__init__.py +0 -0
  53. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/base/namespaces/__init__.py +0 -0
  54. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/base/namespaces/plot.py +0 -0
  55. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/base/namespaces/stats.py +0 -0
  56. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/base/result.py +0 -0
  57. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/base/view.py +0 -0
  58. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/entities/__init__.py +0 -0
  59. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/entities/mws.py +0 -0
  60. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/entities/tehsil.py +0 -0
  61. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/entities/waterbody.py +0 -0
  62. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/export/__init__.py +0 -0
  63. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/export/formats.py +0 -0
  64. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/py.typed +0 -0
  65. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/schema/__init__.py +0 -0
  66. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/schema/detection.py +0 -0
  67. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/schema/profile.py +0 -0
  68. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/utils/__init__.py +0 -0
  69. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/utils/paths.py +0 -0
  70. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/utils/polars_utils.py +0 -0
  71. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/utils/season.py +0 -0
  72. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/src/core_lens/utils/spatial.py +0 -0
  73. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/fixtures/generate_fixtures.py +0 -0
  74. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/conftest.py +0 -0
  75. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_aoi.py +0 -0
  76. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_entities.py +0 -0
  77. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_entity.py +0 -0
  78. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_export.py +0 -0
  79. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_main.py +0 -0
  80. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_plot.py +0 -0
  81. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_polars_utils.py +0 -0
  82. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_profile.py +0 -0
  83. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_result.py +0 -0
  84. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_schema_detection.py +0 -0
  85. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_schema_profile.py +0 -0
  86. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_season.py +0 -0
  87. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_season_config.py +0 -0
  88. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_spatial.py +0 -0
  89. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_stats.py +0 -0
  90. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/tests/unit/test_view.py +0 -0
  91. {core_lens-0.1.dev132 → core_lens-0.1.dev133}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-lens
3
- Version: 0.1.dev132
3
+ Version: 0.1.dev133
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.dev132'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev132')
21
+ __version__ = version = '0.1.dev133'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev133')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -11,6 +11,7 @@ Plugin authors import from the public surface::
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ import functools
14
15
  import pathlib
15
16
  from abc import ABC, abstractmethod
16
17
  from typing import TYPE_CHECKING, Any
@@ -35,6 +36,96 @@ if TYPE_CHECKING:
35
36
  from core_lens.schema.profile import SchemaProfile
36
37
 
37
38
 
39
+ # ---------------------------------------------------------------------------
40
+ # Process-level caches — shared across all BaseEntity instances in a process.
41
+ #
42
+ # Both caches are backed by functools.cache (lru_cache with maxsize=None).
43
+ # maxsize=None means unbounded — intentional: evicting an index or schema
44
+ # profile forces an expensive rebuild on the next access.
45
+ #
46
+ # _cached_detect eliminates repeated detect() calls (OPT-2).
47
+ # _cached_build_index eliminates repeated build_bbox_index() calls (OPT-1).
48
+ #
49
+ # Cache diagnostics: _cached_detect.cache_info() / _cached_build_index.cache_info()
50
+ # Cache reset (tests): _cached_detect.cache_clear() / _cached_build_index.cache_clear()
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ def _so_key(storage_options: dict[str, Any]) -> tuple[tuple[str, Any], ...]:
55
+ """Convert a ``storage_options`` dict to a hashable, order-stable tuple.
56
+
57
+ All args passed to :func:`_cached_detect` and :func:`_cached_build_index`
58
+ must be hashable (``functools.cache`` requirement). ``storage_options``
59
+ is a plain dict, so we convert it here before passing to the cached
60
+ functions. Different cloud credentials for the same path produce
61
+ different cache keys.
62
+
63
+ Args:
64
+ storage_options (dict[str, Any]): Cloud credential / configuration dict.
65
+
66
+ Returns:
67
+ tuple[tuple[str, Any], ...]: A sorted tuple of ``(key, value)`` pairs.
68
+ """
69
+ return tuple(sorted(storage_options.items()))
70
+
71
+
72
+ @functools.cache
73
+ def _cached_detect(
74
+ static_path: str,
75
+ key_cols: tuple[str, ...],
76
+ geometry_col: str,
77
+ annual_path: str | None,
78
+ fortnightly_path: str | None,
79
+ storage_options_key: tuple[tuple[str, Any], ...],
80
+ ) -> "SchemaProfile":
81
+ """Cached wrapper around :func:`~core_lens.schema.detection.detect`.
82
+
83
+ All arguments must be hashable so they can form the cache key. Pass
84
+ ``storage_options`` through :func:`_so_key` before calling this function.
85
+
86
+ Returns:
87
+ SchemaProfile: Detected schema profile for the given entity paths.
88
+ """
89
+ from core_lens.schema.detection import detect
90
+
91
+ return detect(
92
+ static_path=static_path,
93
+ key_cols=list(key_cols),
94
+ geometry_col=geometry_col,
95
+ annual_path=annual_path,
96
+ fortnightly_path=fortnightly_path,
97
+ storage_options=dict(storage_options_key) or None,
98
+ )
99
+
100
+
101
+ @functools.cache
102
+ def _cached_build_index(
103
+ static_path: str,
104
+ key_cols: tuple[str, ...],
105
+ bbox_cols: tuple[str, str, str, str] | None,
106
+ geometry_col: str,
107
+ geometry_type: str,
108
+ storage_options_key: tuple[tuple[str, Any], ...],
109
+ ) -> pl.DataFrame:
110
+ """Cached wrapper around :func:`~core_lens.utils.spatial.build_bbox_index`.
111
+
112
+ All arguments must be hashable so they can form the cache key. Pass
113
+ ``storage_options`` through :func:`_so_key` before calling this function.
114
+
115
+ Returns:
116
+ pl.DataFrame: The bounding-box index DataFrame.
117
+ """
118
+ logger.debug("Building bbox index for {} (cache miss)", static_path)
119
+ return build_bbox_index(
120
+ static_path=static_path,
121
+ key_cols=list(key_cols),
122
+ bbox_cols=bbox_cols,
123
+ geometry_col=geometry_col,
124
+ geometry_type=geometry_type,
125
+ storage_options=dict(storage_options_key) or None,
126
+ )
127
+
128
+
38
129
  class BaseEntity(ABC):
39
130
  """Abstract base class for every entity in the core_lens plugin system.
40
131
 
@@ -228,19 +319,29 @@ class BaseEntity(ABC):
228
319
  def schema_profile(self) -> "SchemaProfile":
229
320
  """Validated schema descriptor for this entity's data files.
230
321
 
231
- Auto-detected from Parquet file metadata on first access and cached on
232
- the instance. Override in subclasses to provide an explicit profile
233
- instead of relying on detection.
322
+ Auto-detected from Parquet file metadata on first access. The result
323
+ is cached at two levels:
324
+
325
+ 1. **Instance level** — ``_schema_profile`` attribute set on ``self``
326
+ so that repeated property access within the same instance is a bare
327
+ attribute lookup with no dict overhead.
328
+ 2. **Process level** — :func:`_cached_detect` (backed by
329
+ ``functools.cache``) keyed on the resolved paths and entity
330
+ configuration. Different ``AoI()`` calls that point at the same
331
+ data directory share one ``SchemaProfile`` object, avoiding
332
+ redundant ``collect_schema()`` I/O.
333
+
334
+ Override in subclasses to provide an explicit profile instead of
335
+ relying on detection.
234
336
 
235
337
  Returns:
236
338
  SchemaProfile: A fully-validated :class:`~core_lens.schema.profile.SchemaProfile`.
237
339
  """
238
340
  if not hasattr(self, "_schema_profile"):
239
- from core_lens.schema.detection import detect
240
-
241
- self._schema_profile: SchemaProfile = detect(
341
+ _so = self._storage_options or {}
342
+ self._schema_profile: SchemaProfile = _cached_detect(
242
343
  static_path=self._resolve(self.static_path),
243
- key_cols=self.key_cols,
344
+ key_cols=tuple(self.key_cols),
244
345
  geometry_col=self.geometry_col,
245
346
  annual_path=(
246
347
  self._resolve(self.annual_path)
@@ -252,24 +353,36 @@ class BaseEntity(ABC):
252
353
  if self.fortnightly_path is not None
253
354
  else None
254
355
  ),
255
- storage_options=self._storage_options or None,
356
+ storage_options_key=_so_key(_so),
256
357
  )
257
358
  return self._schema_profile
258
359
 
259
360
  @property
260
361
  def _index(self) -> pl.DataFrame:
362
+ """In-memory bounding-box index for spatial pre-filtering.
363
+
364
+ Cached at two levels (same strategy as :attr:`schema_profile`):
365
+
366
+ 1. **Instance level** — ``_cached_index`` attribute on ``self``.
367
+ 2. **Process level** — :func:`_cached_build_index` (backed by
368
+ ``functools.cache``) keyed on the resolved static path, key
369
+ columns, and bbox/geometry schema. All entity instances that
370
+ resolve to the same static file share one index ``pl.DataFrame``,
371
+ eliminating repeated geometry decodes.
372
+
373
+ Returns:
374
+ pl.DataFrame: DataFrame with columns ``(*key_cols, minx, miny, maxx, maxy)``.
375
+ """
261
376
  if not hasattr(self, "_cached_index"):
262
- logger.debug(
263
- "Building lazy bounding box index for {}", self.__class__.__name__
264
- )
265
- profile = self.schema_profile
266
- self._cached_index: pl.DataFrame = build_bbox_index(
377
+ profile = self.schema_profile # already cached — no extra I/O
378
+ _so = self._storage_options or {}
379
+ self._cached_index: pl.DataFrame = _cached_build_index(
267
380
  static_path=self._resolve(self.static_path),
268
- key_cols=self.key_cols,
381
+ key_cols=tuple(self.key_cols),
269
382
  bbox_cols=profile.bbox_cols,
270
383
  geometry_col=profile.geometry_col,
271
384
  geometry_type=profile.geometry_type,
272
- storage_options=self._storage_options or None,
385
+ storage_options_key=_so_key(_so),
273
386
  )
274
387
  return self._cached_index
275
388
 
File without changes
File without changes
File without changes
File without changes