core-lens 0.1.dev172__tar.gz → 0.1.dev175__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.dev172 → core_lens-0.1.dev175}/PKG-INFO +1 -1
  2. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/README.md +0 -1
  3. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_entity.py +0 -1
  4. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_polars_utils.py +1 -1
  5. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_result.py +5 -2
  6. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_schema.py +4 -2
  7. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_spatial.py +0 -1
  8. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_view.py +8 -5
  9. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/run_all.sh +1 -1
  10. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/_version.py +2 -2
  11. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/namespaces/plot.py +32 -18
  12. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/result.py +13 -2
  13. core_lens-0.1.dev172/benchmarks/bench_paths.py +0 -217
  14. core_lens-0.1.dev172/parse_scalene.py +0 -50
  15. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  16. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  17. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  18. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/pull_request_template.md +0 -0
  19. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/workflows/ci.yml +0 -0
  20. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/workflows/gh-pages.yml +0 -0
  21. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/workflows/pre-release.yml +0 -0
  22. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.github/workflows/release.yml +0 -0
  23. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.gitignore +0 -0
  24. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.gitmessage +0 -0
  25. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.pre-commit-config.yaml +0 -0
  26. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/.python-version +0 -0
  27. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/CONTRIBUTING.md +0 -0
  28. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/LICENSE +0 -0
  29. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/README.md +0 -0
  30. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/SKILLS.md +0 -0
  31. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_aoi.py +0 -0
  32. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_export.py +0 -0
  33. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/benchmarks/bench_season.py +0 -0
  34. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/Makefile +0 -0
  35. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/make.bat +0 -0
  36. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/concepts.md +0 -0
  37. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/conf.py +0 -0
  38. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/export.md +0 -0
  39. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/index.rst +0 -0
  40. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/intro.md +0 -0
  41. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/logging.md +0 -0
  42. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/plots.md +0 -0
  43. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/plugins.md +0 -0
  44. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/queries.md +0 -0
  45. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/quickstart.md +0 -0
  46. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/docs/source/stats.md +0 -0
  47. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/examples/demo_mws.py +0 -0
  48. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/examples/demo_tehsil.py +0 -0
  49. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/hooks/mypy.sh +0 -0
  50. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/hooks/no-parquet-outside-fixtures.sh +0 -0
  51. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/hooks/pytest.sh +0 -0
  52. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/pyproject.toml +0 -0
  53. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/__init__.py +0 -0
  54. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/__main__.py +0 -0
  55. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/aoi.py +0 -0
  56. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/__init__.py +0 -0
  57. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/entity.py +0 -0
  58. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/namespaces/__init__.py +0 -0
  59. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/namespaces/stats.py +0 -0
  60. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/base/view.py +0 -0
  61. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/entities/__init__.py +0 -0
  62. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/entities/mws.py +0 -0
  63. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/entities/tehsil.py +0 -0
  64. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/entities/waterbody.py +0 -0
  65. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/export/__init__.py +0 -0
  66. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/export/formats.py +0 -0
  67. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/py.typed +0 -0
  68. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/schema/__init__.py +0 -0
  69. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/schema/detection.py +0 -0
  70. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/schema/profile.py +0 -0
  71. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/__init__.py +0 -0
  72. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/paths.py +0 -0
  73. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/polars_utils.py +0 -0
  74. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/season.py +0 -0
  75. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/src/core_lens/utils/spatial.py +0 -0
  76. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/fixtures/generate_fixtures.py +0 -0
  77. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/conftest.py +0 -0
  78. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_aoi.py +0 -0
  79. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_entities.py +0 -0
  80. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_entity.py +0 -0
  81. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_export.py +0 -0
  82. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_main.py +0 -0
  83. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_plot.py +0 -0
  84. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_polars_utils.py +0 -0
  85. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_profile.py +0 -0
  86. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_result.py +0 -0
  87. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_schema_detection.py +0 -0
  88. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_schema_profile.py +0 -0
  89. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_season.py +0 -0
  90. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_season_config.py +0 -0
  91. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_spatial.py +0 -0
  92. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_stats.py +0 -0
  93. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/tests/unit/test_view.py +0 -0
  94. {core_lens-0.1.dev172 → core_lens-0.1.dev175}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-lens
3
- Version: 0.1.dev172
3
+ Version: 0.1.dev175
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
@@ -59,7 +59,6 @@ uv run python benchmarks/bench_spatial.py
59
59
  | `bench_aoi.py` | `AoI.register`, `AoI.__init__` (bbox/geometry/named/seasons), `__getattr__`, `current_season/year`, `registered_entities`, `deregister` |
60
60
  | `bench_entity.py` | `_resolve` (local/cloud), `where` (single/composite/list/error), `spatial_filter` (bbox/geometry/area/error), `spatial_join`, `schema_profile` cache, `_index` cache, `geometry_lazy`, `_so_key`, `_entity_name`, `_cached_detect`, `EntityValidationError` |
61
61
  | `bench_spatial.py` | `build_bbox_index`, `bbox_intersects_geometry`, `exact_spatial_filter` (centroid/area/all-India), `execute_spatial_join`, `_bbox_sidecar_path`, `_read_bbox_sidecar`, error path |
62
- | `bench_paths.py` | `is_cloud_uri` (positive/negative), `resolve_fs_and_path` (local/cloud), `path_exists` (exists/absent), `join_uri` (local/cloud) |
63
62
  | `bench_view.py` | `where` (single/composite), `spatial_filter` (bbox), `between` (dates/season/CURRENT/year-range/errors), `spatial_join` (spec/double-error), `static`/`annual`/`sub_annual` materialisation |
64
63
  | `bench_result.py` | `df`, `lazy`, `gdf` (WKB decode), `materialise`, `derive` (single/chain), `with_geometry` (noop/annual), `aggregate` (no-by/year/month/season/year_month/season_year/errors), `gdf` TypeError guard, `stats`/`plot` accessors |
65
64
  | `bench_season.py` | `SeasonConfig.__post_init__` (default/custom), `season_for`, `resolve_time_filter` (date-range/kharif/rabi/year-range), `add_temporal_columns` (first/second noop call), Polars filter execution |
@@ -38,7 +38,6 @@ from core_lens.base.entity import (
38
38
  _so_key,
39
39
  _entity_name,
40
40
  _cached_detect,
41
- _cached_build_index,
42
41
  EntityValidationError,
43
42
  )
44
43
 
@@ -191,7 +191,7 @@ print(
191
191
  # ── 7. cached_read_schema() — cold vs cached ──────────────────────────────────
192
192
  _section("7. cached_read_schema() [cold vs functools.cache warm]")
193
193
  from core_lens.utils.polars_utils import cached_read_schema # noqa: E402
194
- from core_lens.utils.polars_utils import _cached_schema_internal # type: ignore[attr-defined] # noqa: E402
194
+ from core_lens.utils.polars_utils import _cached_schema_internal # noqa: E402
195
195
 
196
196
  # Warm the cache first (may already be warm from module import chain).
197
197
  cached_read_schema(static_path)
@@ -188,7 +188,8 @@ print(
188
188
 
189
189
  # ── 10. Result.aggregate(by='season') / by='month' / by='year_month' ─────────
190
190
  _section("10. Result.aggregate() sub_annual groupings")
191
- from core_lens.base.view import Season
191
+ from core_lens.base.view import Season # noqa: E402
192
+
192
193
  result_fn = aoi_small.mws.between(season=Season.KHARIF, year=(2018, 2022)).sub_annual
193
194
 
194
195
  data_col = result_fn.columns[-1] # last column (data column)
@@ -197,7 +198,9 @@ for by_key in ("month", "season", "year_month", "season_year", "year"):
197
198
  t0 = time.perf_counter()
198
199
  agg_r = result_fn.aggregate(pl.first(data_col), by=by_key)
199
200
  t1 = time.perf_counter()
200
- print(f"aggregate(by={by_key!r:12}) : {(t1 - t0) * 1000:.2f} ms shape={agg_r.df().shape}")
201
+ print(
202
+ f"aggregate(by={by_key!r:12}) : {(t1 - t0) * 1000:.2f} ms shape={agg_r.df().shape}"
203
+ )
201
204
 
202
205
 
203
206
  # ── 11. Result.aggregate() error paths ──────────────────────────────────
@@ -221,7 +221,7 @@ else:
221
221
 
222
222
  # ── 9. _find_lon_companion() ─────────────────────────────────────────────────
223
223
  _section("9. _find_lon_companion() [lat→lon heuristic — O(candidates)]")
224
- import polars as pl # noqa: E401 (already imported; re-stated for Scalene trace clarity)
224
+ import polars as pl # noqa: E401, E402 (already imported; re-stated for Scalene trace clarity)
225
225
 
226
226
  # Build a synthetic schema with lat + lon columns to exercise the positive branch.
227
227
  synthetic_schema = pl.Schema(
@@ -265,7 +265,9 @@ print("_require_cols (ok) : no error (expect no error)")
265
265
 
266
266
  # ── 11. SchemaProfile.from_file() — JSON round-trip ──────────────────────────
267
267
  _section("11. SchemaProfile.from_file() [JSON round-trip]")
268
- import json, tempfile, pathlib
268
+ import json # noqa: E402
269
+ import tempfile # noqa: E402
270
+ import pathlib # noqa: E402
269
271
 
270
272
  profile2 = entity2.schema_profile
271
273
  profile_dict = profile2.model_dump()
@@ -193,7 +193,6 @@ print(f"spatial_filter (geom) : {(t1 - t0) * 1000:.2f} ms")
193
193
 
194
194
  # ── 9. execute_spatial_join() — cross-entity agg join ────────────────────────
195
195
  _section("9. execute_spatial_join() [cross-entity agg on small bbox]")
196
- import polars as pl # already imported at module top — re-imported for clarity
197
196
  from core_lens.utils.spatial import execute_spatial_join # noqa: E402
198
197
 
199
198
  # Use the small-AoI static data as primary and the same entity as secondary.
@@ -2,7 +2,7 @@
2
2
  =======================================================
3
3
  Targets:
4
4
  - View.where() attribute filter (Polars semi-join on static file)
5
- - View.spatial_filter() STRtree filter narrowing existing keys
5
+ - View.spatial_filter() vectorized centroid test (or STRtree for area)
6
6
  - View.between() time-filter dict construction (no I/O)
7
7
  - View.between(season=CURRENT) current-season resolution
8
8
  - View.between(year=tuple) year-range season filter
@@ -147,7 +147,9 @@ print(f"Shape : {result_annual.df().shape}")
147
147
  # ── 11. sub_annual materialisation ───────────────────────────────────────────
148
148
  _section("11. aoi_small.mws.sub_annual [sub_annual materialisation]")
149
149
  t0 = time.perf_counter()
150
- result_fn = aoi_small.mws.between(season=Season.KHARIF, year=2022).sub_annual.materialise()
150
+ result_fn = aoi_small.mws.between(
151
+ season=Season.KHARIF, year=2022
152
+ ).sub_annual.materialise()
151
153
  t1 = time.perf_counter()
152
154
  print(f"sub_annual : {(t1 - t0) * 1000:.2f} ms")
153
155
  print(f"Shape : {result_fn.df().shape}")
@@ -179,7 +181,6 @@ print(
179
181
 
180
182
  # ── 14. View.between() error paths ───────────────────────────────────────────
181
183
  _section("14. View.between() error paths [guard branches — no I/O]")
182
- import traceback
183
184
 
184
185
  errors_caught = 0
185
186
 
@@ -218,7 +219,7 @@ print(f"Error guards raised : {errors_caught}/5 (expect 5)")
218
219
 
219
220
  # ── 15. View.spatial_join() — deferred cross-entity join spec ─────────────────
220
221
  _section("15. View.spatial_join() [deferred spec — no I/O]")
221
- from core_lens.entities.mws import MWSEntity as _MWS2
222
+ from core_lens.entities.mws import MWSEntity as _MWS2 # noqa: E402
222
223
 
223
224
  other_entity = _MWS2(data_root=DATA_ROOT)
224
225
  REPS_SJ = 10_000
@@ -234,7 +235,9 @@ print(
234
235
  # 15b. Double spatial_join raises ValueError
235
236
  duplicate_raised = False
236
237
  try:
237
- view_all.spatial_join(other_entity, {"x": "sum"}).spatial_join(other_entity, {"y": "mean"})
238
+ view_all.spatial_join(other_entity, {"x": "sum"}).spatial_join(
239
+ other_entity, {"y": "mean"}
240
+ )
238
241
  except ValueError:
239
242
  duplicate_raised = True
240
243
  print(f"Double join guard : {duplicate_raised} (expect True)")
@@ -19,7 +19,7 @@ BENCHMARKS=(
19
19
  "bench_aoi"
20
20
  "bench_entity"
21
21
  "bench_spatial"
22
- "bench_paths"
22
+ # "bench_paths"
23
23
  "bench_view"
24
24
  "bench_result"
25
25
  "bench_season"
@@ -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.dev172'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev172')
21
+ __version__ = version = '0.1.dev175'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev175')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -731,31 +731,45 @@ class PlotNamespace:
731
731
  )
732
732
 
733
733
  entities = df.get_column(key_col).unique().to_list()
734
- for i, entity in enumerate(entities):
735
- row = df.filter(pl.col(key_col) == entity)
736
-
737
- # Replace None with NaN for numeric columns to prevent Bokeh JS rendering crashes
738
- source_data = {}
739
- for c in hover_cols:
740
- if c in row.columns:
741
- lst = row.get_column(c).to_list()
742
- if row.schema[c].is_numeric():
743
- lst = [float("nan") if v is None else v for v in lst]
744
- source_data[c] = lst
745
-
746
- source = ColumnDataSource(source_data) # type: ignore[arg-type]
734
+
735
+ if len(entities) > 20:
736
+ # Fast path: single dataset, single colour, no legend
737
+ pdf = df.drop_nulls(subset=[x, y_col]).select(hover_cols).to_pandas()
738
+ source = ColumnDataSource(pdf)
747
739
  fig.scatter(
748
740
  x=x,
749
741
  y=y_col,
750
742
  source=source,
751
- color=_color_for(i),
752
- legend_label=str(entity),
743
+ color=_color_for(0),
753
744
  size=8,
754
745
  alpha=0.7,
755
746
  )
756
- if fig.legend:
757
- fig.legend.click_policy = "hide"
758
- fig.add_layout(fig.legend[0], "right")
747
+ else:
748
+ for i, entity in enumerate(entities):
749
+ row = df.filter(pl.col(key_col) == entity)
750
+
751
+ # Replace None with NaN for numeric columns to prevent Bokeh JS rendering crashes
752
+ source_data = {}
753
+ for c in hover_cols:
754
+ if c in row.columns:
755
+ lst = row.get_column(c).to_list()
756
+ if row.schema[c].is_numeric():
757
+ lst = [float("nan") if v is None else v for v in lst]
758
+ source_data[c] = lst
759
+
760
+ source = ColumnDataSource(source_data) # type: ignore[arg-type]
761
+ fig.scatter(
762
+ x=x,
763
+ y=y_col,
764
+ source=source,
765
+ color=_color_for(i),
766
+ legend_label=str(entity),
767
+ size=8,
768
+ alpha=0.7,
769
+ )
770
+ if fig.legend:
771
+ fig.legend.click_policy = "hide"
772
+ fig.add_layout(fig.legend[0], "right")
759
773
  _apply_theme(fig, self.result, "Scatter Distribution")
760
774
  return fig
761
775
 
@@ -279,12 +279,23 @@ class Result:
279
279
  )
280
280
 
281
281
  if by is None:
282
- new_data = self.data.group_by(self.key_cols).agg(*exprs)
282
+ group_cols = self.key_cols
283
283
  else:
284
284
  # Temporal grouping columns are expected to already exist on the
285
285
  # frame (added by the materialisation layer from the time column).
286
286
  group_cols = self.key_cols + [by]
287
- new_data = self.data.group_by(group_cols).agg(*exprs)
287
+
288
+ # Polars group_by throws DuplicateError if an aggregation expression
289
+ # outputs a column name that is already present in the grouping keys.
290
+ # Filter them out to prevent crashes when users iterate over all columns.
291
+ safe_exprs = [
292
+ expr
293
+ for expr in exprs
294
+ if getattr(expr, "meta", None) is None
295
+ or expr.meta.output_name() not in group_cols
296
+ ]
297
+
298
+ new_data = self.data.group_by(group_cols).agg(*safe_exprs)
288
299
 
289
300
  return self._replace(data=new_data)
290
301
 
@@ -1,217 +0,0 @@
1
- """bench_paths.py — Benchmark for core_lens.utils.paths
2
- ======================================================
3
- Targets:
4
- - is_cloud_uri() URI scheme prefix check (hot path helper)
5
- - resolve_fs_and_path() URI → (FileSystem, path) resolution
6
- - path_exists() local filesystem existence check
7
- - join_uri() URI-agnostic path join (local + cloud)
8
-
9
- Scalene focuses on:
10
- - string prefix scanning in is_cloud_uri (Python vs C in str.startswith)
11
- - pyarrow.fs.FileSystem.from_uri overhead for cloud URIs
12
- - pathlib.Path normalisation cost in resolve_fs_and_path / join_uri
13
- - pyarrow.fs.get_file_info cost for path_exists on local paths
14
-
15
- Usage:
16
- uv run scalene run --html --outfile benchmarks/profiles/bench_paths.html benchmarks/bench_paths.py
17
- uv run python benchmarks/bench_paths.py
18
- """
19
-
20
- from __future__ import annotations
21
-
22
- import pathlib
23
- import time
24
-
25
- from core_lens.utils.paths import (
26
- is_cloud_uri,
27
- join_uri,
28
- path_exists,
29
- resolve_fs_and_path,
30
- )
31
-
32
- # ── Config ────────────────────────────────────────────────────────────────────
33
- DATA_ROOT = "data/"
34
- LOCAL_PATH = str(pathlib.Path(DATA_ROOT).resolve())
35
-
36
- CLOUD_URIS = [
37
- "s3://geo-benchmark-data/converted/mws/static",
38
- ]
39
- LOCAL_URIS = [
40
- LOCAL_PATH,
41
- "/tmp/nonexistent_path",
42
- "relative/path",
43
- ]
44
-
45
-
46
- def _section(title: str) -> None:
47
- print(f"\n{'─' * 60}")
48
- print(f" {title}")
49
- print("─" * 60)
50
-
51
-
52
- # ── 1. is_cloud_uri() — positive path (cloud URIs) ────────────────────────────
53
- _section("1. is_cloud_uri() [cloud URIs — positive match]")
54
- REPS = 1_000_000
55
-
56
- t0 = time.perf_counter()
57
- for _ in range(REPS):
58
- for uri in CLOUD_URIS:
59
- is_cloud_uri(uri)
60
- t1 = time.perf_counter()
61
- per_call = (t1 - t0) / (REPS * len(CLOUD_URIS))
62
- print(
63
- f"is_cloud_uri (positive) ×{REPS * len(CLOUD_URIS)}: "
64
- f"{(t1 - t0) * 1000:.2f} ms total ({per_call * 1e9:.1f} ns/call)"
65
- )
66
- for uri in CLOUD_URIS:
67
- print(f" {uri[:40]!r:42} → {is_cloud_uri(uri)}")
68
-
69
-
70
- # ── 2. is_cloud_uri() — negative path (local paths) ──────────────────────────
71
- _section("2. is_cloud_uri() [local paths — negative match]")
72
- t0 = time.perf_counter()
73
- for _ in range(REPS):
74
- for uri in LOCAL_URIS:
75
- is_cloud_uri(uri)
76
- t1 = time.perf_counter()
77
- per_call = (t1 - t0) / (REPS * len(LOCAL_URIS))
78
- print(
79
- f"is_cloud_uri (negative) ×{REPS * len(LOCAL_URIS)}: "
80
- f"{(t1 - t0) * 1000:.2f} ms total ({per_call * 1e9:.1f} ns/call)"
81
- )
82
- for uri in LOCAL_URIS:
83
- print(f" {uri!r:42} → {is_cloud_uri(uri)}")
84
-
85
-
86
- # ── 3. resolve_fs_and_path() — local path ─────────────────────────────────────
87
- _section("3. resolve_fs_and_path() [local path — pathlib normalise]")
88
- REPS_FS = 10_000
89
- t0 = time.perf_counter()
90
- for _ in range(REPS_FS):
91
- resolve_fs_and_path(LOCAL_PATH)
92
- t1 = time.perf_counter()
93
- print(
94
- f"resolve_fs (local) ×{REPS_FS}: {(t1 - t0) * 1000:.2f} ms total "
95
- f"({(t1 - t0) / REPS_FS * 1e6:.2f} µs/call)"
96
- )
97
- fs, path = resolve_fs_and_path(LOCAL_PATH)
98
- print(f" fs type : {type(fs).__name__}")
99
- print(f" path : {path!r}")
100
-
101
-
102
- # ── 4. resolve_fs_and_path() — cloud URI ──────────────────────────────────────
103
- _section("4. resolve_fs_and_path() [cloud URI — lru_cache cold vs warm]")
104
- from core_lens.utils.paths import _resolve_fs_and_path_cached # noqa: E402
105
-
106
- cloud_uri = CLOUD_URIS[0] # s3:// URI
107
-
108
- # Cold: flush cache, measure the first call (pafs.FileSystem.from_uri cost).
109
- _resolve_fs_and_path_cached.cache_clear()
110
- t0 = time.perf_counter()
111
- resolve_fs_and_path(cloud_uri)
112
- t1 = time.perf_counter()
113
- t_cold_us = (t1 - t0) * 1e6
114
- print(f"resolve_fs cloud (cold) : {t_cold_us:.2f} µs ← pafs.FileSystem.from_uri")
115
-
116
- # Warm: same URI, now served from lru_cache.
117
- REPS_CFS = 100_000
118
- t0 = time.perf_counter()
119
- for _ in range(REPS_CFS):
120
- resolve_fs_and_path(cloud_uri)
121
- t1 = time.perf_counter()
122
- t_warm_us = (t1 - t0) / REPS_CFS * 1e6
123
- print(
124
- f"resolve_fs cloud (warm) ×{REPS_CFS}: {(t1 - t0) * 1000:.2f} ms total "
125
- f"({t_warm_us:.3f} µs/call)"
126
- )
127
- print(f"Speedup (cold→warm) : {t_cold_us / t_warm_us:.0f}×")
128
- print(f"Cache info : {_resolve_fs_and_path_cached.cache_info()}")
129
-
130
- fs_c, path_c = resolve_fs_and_path(cloud_uri)
131
- print(f" fs type : {type(fs_c).__name__}")
132
- print(f" path : {path_c!r}")
133
-
134
- # All four cloud URI schemes — each is a separate cache entry.
135
- _section("4b. All cloud URI schemes [each scheme = distinct cache entry]")
136
- for uri in CLOUD_URIS:
137
- _resolve_fs_and_path_cached.cache_clear()
138
- t0 = time.perf_counter()
139
- resolve_fs_and_path(uri)
140
- t1 = time.perf_counter()
141
- print(f" cold {uri[:45]!r:47} : {(t1 - t0) * 1e6:.1f} µs")
142
- print(f"Cache info (after 4 URIs): {_resolve_fs_and_path_cached.cache_info()}")
143
-
144
-
145
- # ── 5. path_exists() — local path (exists) ────────────────────────────────────
146
- _section("5. path_exists() [local — directory exists]")
147
- REPS_PE = 1_000
148
- t0 = time.perf_counter()
149
- for _ in range(REPS_PE):
150
- path_exists(LOCAL_PATH)
151
- t1 = time.perf_counter()
152
- print(
153
- f"path_exists (exists) ×{REPS_PE}: {(t1 - t0) * 1000:.2f} ms total "
154
- f"({(t1 - t0) / REPS_PE * 1e6:.2f} µs/call)"
155
- )
156
- print(f" exists: {path_exists(LOCAL_PATH)!r}")
157
-
158
-
159
- # ── 6. path_exists() — local path (absent) ────────────────────────────────────
160
- _section("6. path_exists() [local — path does not exist]")
161
- t0 = time.perf_counter()
162
- for _ in range(REPS_PE):
163
- path_exists("/tmp/core_lens_nonexistent_xyz_123")
164
- t1 = time.perf_counter()
165
- print(
166
- f"path_exists (absent) ×{REPS_PE}: {(t1 - t0) * 1000:.2f} ms total "
167
- f"({(t1 - t0) / REPS_PE * 1e6:.2f} µs/call)"
168
- )
169
- print(f" exists: {path_exists('/tmp/core_lens_nonexistent_xyz_123')!r}")
170
-
171
-
172
- # ── 7. join_uri() — local root ────────────────────────────────────────────────
173
- _section("7. join_uri() [local root — pathlib join]")
174
- REPS_JU = 1_000_000
175
- t0 = time.perf_counter()
176
- for _ in range(REPS_JU):
177
- join_uri(LOCAL_PATH, "mws/static")
178
- t1 = time.perf_counter()
179
- print(
180
- f"join_uri (local) ×{REPS_JU}: {(t1 - t0) * 1000:.2f} ms total "
181
- f"({(t1 - t0) / REPS_JU * 1e9:.1f} ns/call)"
182
- )
183
- print(f" result: {join_uri(LOCAL_PATH, 'mws/static')!r}")
184
-
185
-
186
- # ── 8. join_uri() — cloud root ────────────────────────────────────────────────
187
- _section("8. join_uri() [cloud root — string concat]")
188
- cloud_root = "s3://my-bucket/data"
189
- t0 = time.perf_counter()
190
- for _ in range(REPS_JU):
191
- join_uri(cloud_root, "mws/static")
192
- t1 = time.perf_counter()
193
- print(
194
- f"join_uri (cloud) ×{REPS_JU}: {(t1 - t0) * 1000:.2f} ms total "
195
- f"({(t1 - t0) / REPS_JU * 1e9:.1f} ns/call)"
196
- )
197
- print(f" result: {join_uri(cloud_root, 'mws/static')!r}")
198
-
199
- # Head-to-head: local vs cloud join.
200
- t_local = time.perf_counter()
201
- for _ in range(REPS_JU):
202
- join_uri(LOCAL_PATH, "mws/static")
203
- t_local = time.perf_counter() - t_local
204
-
205
- t_cloud = time.perf_counter()
206
- for _ in range(REPS_JU):
207
- join_uri(cloud_root, "mws/static")
208
- t_cloud = time.perf_counter() - t_cloud
209
-
210
- print(
211
- f"\nHead-to-head ×{REPS_JU}: local={t_local * 1000:.2f} ms "
212
- f"cloud={t_cloud * 1000:.2f} ms "
213
- f"(cloud is {t_cloud / t_local:.2f}× {'faster' if t_cloud < t_local else 'slower'})"
214
- )
215
-
216
-
217
- print("\n✓ bench_paths.py complete")
@@ -1,50 +0,0 @@
1
- import json
2
- import glob
3
- import os
4
-
5
- profiles = glob.glob('/home/akashyap54/workstation/Projects/benchmarks/profiles/*_profile.json')
6
-
7
- for profile in profiles:
8
- name = os.path.basename(profile).replace('_profile.json', '')
9
- try:
10
- d = json.load(open(profile))
11
- except Exception as e:
12
- print(f"Error loading {name}: {e}")
13
- continue
14
-
15
- print(f"\n# {name.upper()}")
16
- print("-" * 40)
17
-
18
- line_stats = []
19
-
20
- for filename, f_data in d.get('files', {}).items():
21
- short_filename = os.path.basename(filename)
22
- for line_data in f_data.get('lines', []):
23
- cpu_percent = line_data.get('n_cpu_percent_c', 0) + line_data.get('n_cpu_percent_python', 0)
24
- malloc_mb = line_data.get('n_malloc_mb', 0)
25
- peak_mb = line_data.get('n_peak_mb', 0)
26
- line_str = line_data.get('line', '').strip()
27
- lineno = line_data.get('lineno', 0)
28
-
29
- if cpu_percent > 0 or malloc_mb > 0 or peak_mb > 0:
30
- line_stats.append({
31
- 'file': short_filename,
32
- 'lineno': lineno,
33
- 'line': line_str[:60] + ('...' if len(line_str) > 60 else ''),
34
- 'cpu': cpu_percent,
35
- 'malloc_mb': malloc_mb,
36
- 'peak_mb': peak_mb
37
- })
38
-
39
- # Top 5 by CPU
40
- print(f"Top 5 lines by CPU %:")
41
- top_cpu = sorted(line_stats, key=lambda x: x['cpu'], reverse=True)[:5]
42
- for x in top_cpu:
43
- print(f" {x['file']}:{x['lineno']} | CPU: {x['cpu']:.1f}% | Line: {x['line']}")
44
-
45
- # Top 5 by Malloc
46
- print(f"\nTop 5 lines by Memory Allocation (MB):")
47
- top_mem = sorted(line_stats, key=lambda x: x['malloc_mb'], reverse=True)[:5]
48
- for x in top_mem:
49
- if x['malloc_mb'] > 0:
50
- print(f" {x['file']}:{x['lineno']} | Malloc: {x['malloc_mb']:.1f} MB | Line: {x['line']}")
File without changes
File without changes
File without changes
File without changes