core-lens 0.1.dev113__tar.gz → 0.1.dev114__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 (75) hide show
  1. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/PKG-INFO +1 -1
  2. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/_version.py +2 -2
  3. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/base/view.py +21 -1
  4. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/utils/season.py +68 -28
  5. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/.github/pull_request_template.md +0 -0
  6. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/.github/workflows/ci.yml +0 -0
  7. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/.github/workflows/gh-pages.yml +0 -0
  8. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/.github/workflows/pre-release.yml +0 -0
  9. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/.github/workflows/release.yml +0 -0
  10. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/.gitignore +0 -0
  11. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/.gitmessage +0 -0
  12. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/.pre-commit-config.yaml +0 -0
  13. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/.python-version +0 -0
  14. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/CONTRIBUTING.md +0 -0
  15. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/LICENSE +0 -0
  16. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/README.md +0 -0
  17. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/SKILLS.md +0 -0
  18. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/Makefile +0 -0
  19. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/make.bat +0 -0
  20. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/source/concepts.md +0 -0
  21. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/source/conf.py +0 -0
  22. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/source/index.rst +0 -0
  23. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/source/intro.md +0 -0
  24. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/source/plots.md +0 -0
  25. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/source/plugins.md +0 -0
  26. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/source/queries.md +0 -0
  27. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/source/quickstart.md +0 -0
  28. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/docs/source/stats.md +0 -0
  29. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/examples/demo_mws.py +0 -0
  30. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/examples/demo_tehsil.py +0 -0
  31. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/hooks/mypy.sh +0 -0
  32. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/hooks/no-parquet-outside-fixtures.sh +0 -0
  33. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/hooks/pytest.sh +0 -0
  34. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/pyproject.toml +0 -0
  35. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/__init__.py +0 -0
  36. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/__main__.py +0 -0
  37. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/aoi.py +0 -0
  38. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/base/__init__.py +0 -0
  39. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/base/entity.py +0 -0
  40. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/base/namespaces/__init__.py +0 -0
  41. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/base/namespaces/plot.py +0 -0
  42. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/base/namespaces/stats.py +0 -0
  43. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/base/result.py +0 -0
  44. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/entities/__init__.py +0 -0
  45. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/entities/mws.py +0 -0
  46. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/entities/tehsil.py +0 -0
  47. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/entities/waterbody.py +0 -0
  48. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/export/__init__.py +0 -0
  49. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/export/formats.py +0 -0
  50. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/py.typed +0 -0
  51. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/schema/__init__.py +0 -0
  52. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/schema/detection.py +0 -0
  53. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/schema/profile.py +0 -0
  54. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/utils/__init__.py +0 -0
  55. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/utils/polars_utils.py +0 -0
  56. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/src/core_lens/utils/spatial.py +0 -0
  57. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/fixtures/generate_fixtures.py +0 -0
  58. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/conftest.py +0 -0
  59. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_aoi.py +0 -0
  60. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_entities.py +0 -0
  61. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_entity.py +0 -0
  62. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_export.py +0 -0
  63. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_main.py +0 -0
  64. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_plot.py +0 -0
  65. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_polars_utils.py +0 -0
  66. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_profile.py +0 -0
  67. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_result.py +0 -0
  68. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_schema_detection.py +0 -0
  69. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_schema_profile.py +0 -0
  70. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_season.py +0 -0
  71. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_season_config.py +0 -0
  72. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_spatial.py +0 -0
  73. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_stats.py +0 -0
  74. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/tests/unit/test_view.py +0 -0
  75. {core_lens-0.1.dev113 → core_lens-0.1.dev114}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-lens
3
- Version: 0.1.dev113
3
+ Version: 0.1.dev114
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.dev113'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev113')
21
+ __version__ = version = '0.1.dev114'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev114')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -376,7 +376,27 @@ class View:
376
376
  from core_lens.aoi import SeasonConfig
377
377
 
378
378
  season_cfg = self._season_config or SeasonConfig()
379
- time_expr = resolve_time_filter(self.time_filter, time_col, season_cfg)
379
+
380
+ # Detect whether the time column is an integer year column so
381
+ # _date_range_expr can branch at expression-build time (avoids
382
+ # the cast-to-string + runtime when/then triple-copy).
383
+ try:
384
+ pq_schema = pl.read_parquet_schema(abs_path)
385
+ _time_dtype = pq_schema.get(time_col)
386
+ _is_year_col = _time_dtype is not None and _time_dtype in (
387
+ pl.Int32,
388
+ pl.Int64,
389
+ pl.UInt32,
390
+ pl.UInt16,
391
+ )
392
+ except Exception:
393
+ _is_year_col = False
394
+
395
+ # Pass as a private hint inside the filter dict (non-mutating copy).
396
+ _tf = dict(self.time_filter)
397
+ if "start" in _tf and "end" in _tf:
398
+ _tf["_is_year_col"] = _is_year_col
399
+ time_expr = resolve_time_filter(_tf, time_col, season_cfg)
380
400
 
381
401
  lf = scan_with_key_filter(
382
402
  path=abs_path,
@@ -47,7 +47,12 @@ def resolve_time_filter(
47
47
  end = time_filter["end"]
48
48
  assert isinstance(start, str)
49
49
  assert isinstance(end, str)
50
- return _date_range_expr(time_col, start, end)
50
+ # _is_year_col is injected by _materialise when the parquet schema is
51
+ # available. When absent (e.g. direct test calls) pass None so that
52
+ # _date_range_expr uses its runtime when/then fallback.
53
+ _raw = time_filter.get("_is_year_col")
54
+ is_year_col: bool | None = bool(_raw) if _raw is not None else None
55
+ return _date_range_expr(time_col, start, end, is_year_col=is_year_col)
51
56
 
52
57
  if "season" in time_filter:
53
58
  season_name = time_filter["season"]
@@ -64,16 +69,25 @@ def resolve_time_filter(
64
69
  )
65
70
 
66
71
 
67
- def _date_range_expr(time_col: str, start: str, end: str) -> pl.Expr:
72
+ def _date_range_expr(
73
+ time_col: str,
74
+ start: str,
75
+ end: str,
76
+ is_year_col: bool | None = None,
77
+ ) -> pl.Expr:
68
78
  """Return a Polars expression for an inclusive ISO-8601 date range.
69
79
 
70
- Handles both ``pl.Date`` and ``pl.Int``/``pl.Int64`` time columns (year
71
- integers stored as integers rather than dates).
80
+ When ``is_year_col`` is known (``True`` / ``False``) branches at *build
81
+ time* no runtime copies of the column. When ``None`` (caller has no
82
+ schema info) falls back to a ``when/then`` that handles both integer-year
83
+ and Date columns at runtime.
72
84
 
73
85
  Args:
74
86
  time_col: Name of the time column.
75
87
  start: ISO-8601 start date string (``"YYYY-MM-DD"``).
76
88
  end: ISO-8601 end date string (``"YYYY-MM-DD"``).
89
+ is_year_col: ``True`` → integer calendar-year column; ``False`` →
90
+ ``pl.Date`` column; ``None`` → unknown, use runtime detection.
77
91
 
78
92
  Returns:
79
93
  A Polars expression.
@@ -82,25 +96,25 @@ def _date_range_expr(time_col: str, start: str, end: str) -> pl.Expr:
82
96
  end_date = datetime.date.fromisoformat(end)
83
97
  col = pl.col(time_col)
84
98
 
85
- # We use cast to string to avoid ComputeError when the column is an integer,
86
- # but still allow exact date comparison.
87
- # For integer year columns (e.g., 2020), cast to string yields "2020".
88
- # For date columns, cast to string yields "2020-01-01".
89
- # So we compare strings directly! Lexicographical string comparison works perfectly for ISO-8601 dates and 4-digit years.
90
- # For year integers, "2020" >= "2020-01-01" is False.
91
- # Wait, "2020" < "2020-01-01". So if it's an integer, "2020" will not be between "2020-01-01" and "2023-12-31" because "2020" is less than "2020-01-01".
92
- # To fix this, we can extract the year for the integer comparison using a regex or length check.
93
- # Better: check the length of the casted string. If 4, it's a year.
94
- s_col = col.cast(pl.String)
95
- is_year_int = s_col.str.len_bytes() == 4
99
+ if is_year_col is True:
100
+ # Integer year column compare year integers directly, no string cast.
101
+ return col.is_between(start_date.year, end_date.year)
96
102
 
97
- # Use strict=False so "2020-01-01" casts to null instead of raising ComputeError
98
- year_expr = s_col.cast(pl.Int32, strict=False).is_between(
99
- start_date.year, end_date.year
100
- )
101
- date_expr = s_col.is_between(pl.lit(start), pl.lit(end))
103
+ if is_year_col is False:
104
+ # Date column — compare against date literals directly.
105
+ return col.is_between(pl.lit(start_date), pl.lit(end_date))
102
106
 
103
- return pl.when(is_year_int).then(year_expr).otherwise(date_expr)
107
+ # is_year_col is None — dtype unknown (e.g. direct test call without schema).
108
+ # Use a runtime when/then that handles both representations.
109
+ s_col = col.cast(pl.String)
110
+ is_year_like = s_col.str.len_bytes() == 4
111
+ return (
112
+ pl.when(is_year_like)
113
+ .then(
114
+ col.cast(pl.Int32, strict=False).is_between(start_date.year, end_date.year)
115
+ )
116
+ .otherwise(col.is_between(pl.lit(start_date), pl.lit(end_date)))
117
+ )
104
118
 
105
119
 
106
120
  def _season_expr(
@@ -227,17 +241,43 @@ def add_temporal_columns(
227
241
  df = df.with_columns(exprs)
228
242
 
229
243
  # --- season / season_year -----------------------------------------------
230
- # Built via a Python map so SeasonConfig's exact date ranges are used.
231
- # This is the only correct way to handle year-crossing seasons like rabi.
244
+ # Vectorized via Polars when/then on MM-DD string no Python loop,
245
+ # no .to_list(), no Python date objects per row.
232
246
  if "season" not in existing or "season_year" not in existing:
233
- season_labels = [season_config.season_for(d) for d in df[time_col].to_list()]
247
+ date_col = pl.col(time_col)
248
+ md = (
249
+ date_col.dt.month().cast(pl.String).str.pad_start(2, "0")
250
+ + pl.lit("-")
251
+ + date_col.dt.day().cast(pl.String).str.pad_start(2, "0")
252
+ )
253
+ k_start, k_end = season_config.kharif
254
+ r_start, r_end = season_config.rabi
255
+ z_start, z_end = season_config.zaid
256
+
257
+ def _md_between(expr: pl.Expr, start: str, end: str) -> pl.Expr:
258
+ """True when *expr* (MM-DD string) falls within [start, end], handling year rollover."""
259
+ if start <= end:
260
+ # is_between with bare strings → column-name lookup; use pl.lit().
261
+ return expr.is_between(pl.lit(start), pl.lit(end))
262
+ # Year-crossing: e.g. rabi 11-01 → 03-31
263
+ return (expr >= pl.lit(start)) | (expr <= pl.lit(end))
264
+
265
+ season_expr = (
266
+ pl.when(_md_between(md, k_start, k_end))
267
+ .then(pl.lit("kharif"))
268
+ .when(_md_between(md, r_start, r_end))
269
+ .then(pl.lit("rabi"))
270
+ .otherwise(pl.lit("zaid"))
271
+ .alias("season")
272
+ )
273
+
234
274
  if "season" not in existing:
235
- df = df.with_columns(pl.Series("season", season_labels, dtype=pl.String))
275
+ df = df.with_columns(season_expr)
236
276
  if "season_year" not in existing:
237
- years = df["year"].to_list()
238
- season_year_labels = [f"{s}_{y}" for s, y in zip(season_labels, years)]
239
277
  df = df.with_columns(
240
- pl.Series("season_year", season_year_labels, dtype=pl.String)
278
+ (pl.col("season") + pl.lit("_") + pl.col("year").cast(pl.String)).alias(
279
+ "season_year"
280
+ )
241
281
  )
242
282
 
243
283
  return df
File without changes
File without changes
File without changes
File without changes