Repository navigation
Commit da3b1a9
authored
Add Datasets module for managing geospatial and tabular data uploads (#45)
* feat(gis_datasets): catalog module for uploading & managing geospatial datasets
Adds a new ``gis_datasets`` module that lets users upload arbitrary GIS
files (GeoJSON, Shapefile, KML, GeoTIFF, CSV, etc.), browse them with
extracted metadata (CRS, bbox, feature/band counts), and download them.
* Local-filesystem storage abstracted behind ``LocalDatasetStorage`` so
an S3 backend can drop in later without touching the service.
* Best-effort metadata extraction (stdlib JSON for GeoJSON; optional
``fiona``/``rasterio`` paths for Shapefile/KML/raster) — never blocks
an upload when a parser is missing.
* REST API (``/api/gis_datasets``) + Inertia views (Browse/Create/Edit/Show),
permissions group, sidebar menu item, locale namespace, and a health
check that probes the storage dir.
* Alembic migration for the new ``gis_datasets_dataset`` table with
``branch_labels=("gis_datasets",)``.
Framework changes:
* ``i18n_manifest._serialize`` emits single-quoted TS string literals to
match the project's biome ``quoteStyle: "single"``, so the generated
``keys.generated.ts`` is biome-clean without a follow-up format pass.
* ``test_health.test_ready_no_checks_is_healthy`` now tolerates module-
registered checks (the old assertion broke whenever a new module
registered one).
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* refactor(datasets): rename gis_datasets module to datasets
Renames the module so it can catalog any dataset kind (not only GIS).
Mechanical changes only — no behaviour changes:
* ``modules/gis_datasets/`` → ``modules/datasets/`` (and inner
``gis_datasets/`` package → ``datasets/``).
* Class names: ``GisDatasetsModule`` → ``DatasetsModule``,
``GisDatasetsServices`` → ``DatasetsServices``,
``GisDatasetsSettings`` → ``DatasetsSettings``.
* Module meta: ``"GisDatasets"`` → ``"Datasets"``; routes flip from
``/api/gis_datasets`` and ``/gis_datasets`` to ``/api/datasets`` and
``/datasets``; Inertia namespace ``Datasets/Browse`` etc.
* Permissions: ``gis_datasets.{view,upload,edit,delete}`` →
``datasets.*`` (group label "Datasets").
* Settings env prefix: ``SM_GIS_DATASETS_*`` → ``SM_DATASETS_*``;
default storage dir ``./var/datasets``.
* Locale namespace ``gis_datasets`` → ``datasets``; sidebar label
"Datasets".
* Health check name ``datasets.storage``.
Database:
* Drops the ``gis_datasets_dataset`` migration and replaces it with
``add_datasets_table`` creating ``datasets_dataset`` (table prefix
follows the new module name; SQLite parity intact). Migration carries
``branch_labels=("datasets",)``.
All 585 tests still pass; lint and ``make doctor`` clean.
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* feat(datasets): expand public contract surface for consumer modules
Makes the Datasets module easy to depend on. Downstream modules now
have a single stable import path (``datasets.contracts``) and a
one-line FastAPI dependency.
Service protocol ``IDatasetService`` gains:
* ``get_by_slug(slug)`` — slug is the URL-friendly handle other modules
are most likely to persist as a foreign reference.
* ``list_by_kind(kind, *, limit=None)`` — lets consumers filter to just
the kinds they care about (e.g. a tiling module only fetches rasters).
* ``get_file(dataset_id)`` → ``DatasetFile`` — opaque handle bundling
the on-disk path, filename, mime type, and full ``DatasetOut``
metadata so a consumer can open/parse bytes without reaching into
``datasets.storage`` (which would trip SM009).
Public value types:
* ``DatasetFile`` — ``path`` + ``metadata`` + ``open()`` helper; designed
to survive a future S3 storage backend swap.
* URL builders ``download_url`` / ``detail_url`` / ``show_url`` plus
``API_PREFIX`` / ``VIEW_PREFIX`` constants, so dependent UIs don't
hard-code ``/api/datasets/...``.
Events carry slug:
* ``DatasetUploaded`` and ``DatasetDeleted`` now include ``slug`` so
subscribers can route/index without a round-trip to the service.
Dependency ergonomics:
* ``DatasetServiceDep`` ``Annotated`` alias — consuming endpoints write
``datasets: DatasetServiceDep`` instead of
``Depends(get_dataset_service)``.
``datasets.contracts.__init__`` now re-exports the whole surface with
a docstring pointing consumers at the right entry points.
Contract tests live in ``tests/test_public_surface.py`` — anything
that fails there is an API-break for dependent modules.
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* build(datasets): add package.json + tsconfig so CI tsc checks the module
Without its own ``tsconfig.json`` the datasets module's .tsx pages
compiled only indirectly via the host's ``moduleGlobs``, so the
``ci-js-typecheck`` Makefile loop skipped them entirely. Type errors
in Browse/Create/Edit/Show would have slipped past CI. Added:
* ``package.json`` — declares the workspace name and UI/Inertia peers
(mirrors ``modules/products/package.json``).
* ``tsconfig.json`` — extends ``@simple-module/tsconfig/base.json`` with
the standard ``@/*`` and ``@simple-module/ui/*`` path aliases.
* ``pyproject.toml`` hatch ``force-include`` so the wheel ships the
module-root ``package.json`` (host discovers JS deps via
``importlib.resources`` after a pip install).
``make ci-js-typecheck`` now runs ``tsc -p modules/datasets/tsconfig.json``.
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* chore(i18n): regenerate keys after merging main
* refactor(datasets): delegate bytes storage to file_storage.StorageBackend
Datasets no longer owns its own filesystem. Bytes live in whatever
``file_storage`` backend is configured (local FS today, S3 tomorrow),
so swapping the host's ``SM_FILE_STORAGE_BACKEND`` to ``s3`` gets every
dataset upload/download/delete S3-native for free.
Module wiring:
* ``meta.depends_on = ["FileStorage"]`` — ensures file_storage's
``register_settings`` runs first, so ``app.state.file_storage.backend``
is populated before any datasets endpoint fires.
* Dropped ``LocalDatasetStorage`` and the datasets-owned storage health
check (file_storage has its own).
* Dropped ``SM_DATASETS_STORAGE_DIR`` — file_storage's settings are the
single source of truth.
Service layer:
* ``DatasetService.__init__`` now takes a ``StorageBackend`` directly.
* Upload path spools to a tempfile (for fiona/rasterio/stdlib extractors
that need a path), then streams bytes to ``backend.put()``.
* Keys live under a ``datasets/`` prefix in the backend namespace so
they don't collide with generic ``file_storage`` uploads.
* Delete cleans up the backend object after the DB row is gone and
tolerates already-missing objects via ``StorageNotFoundError``.
Public contract:
* ``DatasetFile`` is now backend-agnostic — no ``path`` attribute (that
only made sense for local FS). Consumers call ``await file.stream()``,
``await file.read()``, or ``await file.materialize_to_tempfile()`` to
get a local ``Path`` for GIS libraries that insist on one.
* Download endpoint streams via ``StreamingResponse`` and will 302 to a
presigned URL when the backend supports it.
Tests updated to inject a ``FilesystemBackend(root=tmp_path)`` directly;
public-surface tests cover the async ``stream`` / ``materialize_to_*``
pathway that downstream modules will rely on.
All 7 CI items green locally: 741 Python tests, 8 JS tests, lint,
typecheck (Python + TS), file-size, hardcoded-strings, biome.
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* refactor(datasets): drop IDatasetService Protocol (single-impl)
Follows the convention main #39 established: ship a Protocol only for
real extension points (e.g. ``file_storage.StorageBackend`` with a
registry and multiple backends). Datasets has one implementation and
consumers already type-hint against the concrete ``DatasetService``
via the ``DatasetServiceDep`` annotated alias — the Protocol was
ceremony.
* Delete ``datasets/contracts/service.py`` and drop the re-export.
* Tighten docstrings so nothing still points at the old Protocol.
Also regenerates ``keys.generated.ts`` / ``generated-resources.ts`` to
include the new feature_flags keys alongside datasets.
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* feat(ui): add layers icon so Datasets sidebar item renders an icon
Sidebar menu items render their icon via ``NavIcon``'s static SVG map.
My datasets module's ``register_menu_items`` set ``icon="layers"`` but
the map had no entry for that name, so the sidebar showed "Datasets"
as text only while every other module got a glyph. Added a Heroicons
``layers`` path — the sidebar now renders consistently.
Verified visually by running the dev stack and walking through the
full flow: upload a GeoJSON, land on Browse with a populated row,
follow into Show (dl/dt/dd detail card), then Edit (Card + form).
All four datasets pages (Browse, Create, Edit, Show) use the same
PageShell / Card / Table / Empty primitives as Products, Files, and
Users.
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* feat(datasets): defer metadata extraction to a Celery background task
Metadata extraction is the only expensive bit of the upload path —
GeoJSON is fast but ``fiona`` (shapefile/KML) and ``rasterio`` can
block for tens of seconds on large uploads, which is far too long to
hold an HTTP request open. Moved extraction off the request path into
a Celery task that the ``background_tasks`` module's worker picks up
automatically.
Flow on upload:
1. Endpoint spools bytes to a temp file for size-validation purposes.
2. Service persists a ``Dataset`` row with ``extraction_status="pending"``
— no bbox/CRS/feature_count yet — and uploads the bytes to
``file_storage.StorageBackend``.
3. Endpoint enqueues ``datasets.extract_metadata`` via Celery (task
name exported as ``EXTRACT_METADATA_TASK``).
4. Worker calls ``extract_metadata_task(dataset_id)``: pulls bytes from
the backend to a local tempfile, runs ``extract_metadata``, updates
the row via the ``background_tasks.sync_db`` session.
New files:
* ``datasets/tasks.py`` — ``extract_metadata_task`` shared-task. Uses
``asyncio.run`` to drive the async file_storage backend from inside
the sync Celery worker. Autodiscovered by
``background_tasks.celery_app`` — no framework hook.
* ``tests/test_tasks.py`` — unit tests with ``sync_db`` + file_storage
pointed at tmp paths via ``monkeypatch`` env vars. Verifies the task
fills in bbox/CRS/feature_count and degrades to ``not_found`` when
the row has been deleted between enqueue and run.
* ``tests/conftest.py`` — autouse fixture that stubs
``app.state.background_tasks.celery`` with a ``MagicMock`` (mirrors
the admin suite's setup) so the upload endpoint can call
``send_task`` without a live Redis broker in CI.
Module wiring:
* ``meta.depends_on = ["FileStorage", "BackgroundTasks"]``.
* ``datasets/pyproject.toml`` picks up ``background-tasks`` + ``celery``
as explicit deps so ``datasets`` can be installed standalone.
* New ``get_celery`` dep in ``datasets/deps.py`` reads from
``app.state.background_tasks.celery``.
Refactored service:
* ``DatasetService.register_upload`` no longer calls ``extract_metadata``
— it persists the row, uploads bytes, and leaves
``extraction_status="pending"``. The endpoint does the enqueue.
The background_tasks admin UI at ``/admin/background-tasks`` now shows
every dataset upload as a task row, walks it through
``pending → running → success/failed``, and makes it retryable.
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* refactor(datasets): extract magic strings to constants; drop GIS framing
Introduces ``datasets/constants.py`` as the single source of truth for
every module identifier — permissions, menu, routes, pages, Celery task
name, env prefix, table/schema names, dependency module names, kinds,
extraction-status values, defaults. Every other file imports from it
instead of embedding string literals, mirroring the pattern used by
``file_storage``, ``products``, ``background_tasks``, etc.
Files touched:
* ``module.py`` — ``MODULE_PASCAL``, ``ROUTE_PREFIX_*``,
``MODULE_FILE_STORAGE``/``MODULE_BACKGROUND_TASKS`` (for
``depends_on``), ``PERMISSION_GROUP``, ``ALL_PERMISSIONS``, menu
constants.
* ``endpoints/api.py`` — ``PERM_DATASETS_*`` via
``RequiresPermission(...)``, ``TASK_EXTRACT_METADATA`` for the
Celery enqueue, ``ALL_KINDS`` for the kind allow-list.
* ``endpoints/views.py`` — module-local ``_PAGE_*`` string constants
(Name-only literals, required so ``simple_module_core.diagnostics``
SM003 orphan-page check can resolve them via its AST walker);
``constants.PERM_DATASETS_*`` for permissions.
* ``service.py``, ``tasks.py`` — ``ExtractionStatus.*``,
``DEFAULT_*``, ``STORAGE_KEY_PREFIX``, ``TASK_EXTRACT_METADATA``.
* ``models.py``, ``settings.py``, ``extractors.py``, ``schemas.py`` —
``SCHEMA_NAME``, ``TABLE_DATASET``, ``ENV_PREFIX``, ``DatasetKind``
enum, ``DEFAULT_GEOJSON_CRS``, ``DEFAULT_MAX_UPLOAD_MB``.
Also drops the remaining "GIS"/"geospatial" framing from user-visible
strings and docstrings — the catalog now describes itself as a generic
dataset catalog with optional geospatial metadata extraction. The
``fiona``/``rasterio`` library references stay in code comments
because those are the actual library names.
* ``locales/en.json`` — "geospatial datasets" → "datasets" across
browse description, empty state, create description.
* ``models.py`` — "A geospatial dataset ..." → "A dataset ...".
* ``extractors.py`` — module docstring drops "GIS" framing.
* ``contracts/files.py`` — "GIS library" → "parser library" / named
libraries.
All 7 CI items green locally: 816 python tests, 8 JS tests, ruff
format/check, ty, biome, tsc x 11 workspaces, 300-line cap,
``check_hardcoded_strings`` clean, ``make doctor`` shows no SM003
warnings against datasets.
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* feat(datasets): wire feature flags, role mapping, settings registration
Rounds out the module's integration with the framework's governance
primitives. Every identifier lives in ``datasets/constants.py``.
Feature flags (registered via ``register_feature_flags``):
* ``datasets.auto_extract`` (default on) — guards the Celery
``send_task`` call on the upload endpoint. Admins can freeze
auto-extraction during incidents without redeploying; Dataset rows
stay ``pending`` and can be re-triggered later.
* ``datasets.allow_raster_uploads`` (default on) — 422s GeoTIFF
uploads with a clear "raster uploads are disabled" message when
turned off, useful on instances without ``rasterio`` installed.
Permission role mapping (via ``registry.map_role``):
* The ``user`` role now inherits ``datasets.view`` + ``datasets.upload``
so non-admins can browse and self-upload. ``edit`` / ``delete`` stay
admin-only through the framework wildcard.
Settings registration (registered via ``on_startup`` against the
``settings`` module's registry, skipped gracefully if the settings
module isn't installed):
* ``datasets.max_upload_mb`` — runtime override for the per-dataset
upload size. ``get_max_upload_bytes`` reads this on every upload
request, falling back to the env-backed default on the module's
``DatasetsSettings``.
* ``datasets.default_kind`` — the kind to assign when filename-based
detection falls back to ``other``.
Resilience fix:
* ``send_task`` wrapped in a logger-suppressed try/except. A dead
broker (Redis down, network blip) no longer cascades into a failed
upload — the dataset row is already persisted with
``extraction_status="pending"`` and the failure is logged for
operator retry.
Tests:
* ``tests/test_wiring.py`` covers flag registration, permission role
mapping, setting registration (via ``getattr(app.state, "settings",
None)`` so it skips gracefully), and the flag-toggle behaviours on
upload (auto-extract off → no ``send_task``; raster flag off → 422).
Verified visually with a live dev run — see
``.playwright-cli/video-2026-04-20T17-02-55-633Z.webm`` for the full
walk-through (login → upload → bbox/CRS extraction → browse → show →
feature flags admin → module settings admin).
All 7 CI items green: ruff/ty/biome/tsc, 823 python tests, 8 JS tests,
file-size cap, ``check_hardcoded_strings`` clean, ``make doctor`` 0
errors on datasets.
https://claude.ai/code/session_01W384SpjDjwzuZmQq2WV4Qv
* fix(ci): sync package-lock.json with @simple-module/datasets workspace
CI's ``make install-js`` runs ``npm ci`` which fails when the lock file
disagrees with any workspace ``package.json``. The
``@simple-module/datasets`` workspace was added a while back but its
entry never made it into ``package-lock.json``, so every JS job
(lint, typecheck, tests, build) was failing during install with::
npm error Missing: @simple-module/datasets@0.1.0 from lock file
Local ``npm install`` had been silently fixing this each time I ran it
which is why CI was the only place that surfaced the gap. Re-ran
``npm install`` and committed the resulting lock-file delta (17
lines, just the new workspace's metadata).1 parent e378f91 commit da3b1a9
42 files changed
Lines changed: 3303 additions & 12 deletions
File tree
- framework/hosting
- simple_module_hosting
- tests
- host
- migrations/versions
- modules/datasets
- datasets
- contracts
- endpoints
- locales
- pages
- tests
- packages
- i18n/src
- ui/src/components
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
29 | 33 | | |
30 | 34 | | |
31 | 35 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
82 | 82 | | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
| 83 | + | |
87 | 84 | | |
88 | 85 | | |
89 | 86 | | |
| |||
125 | 122 | | |
126 | 123 | | |
127 | 124 | | |
128 | | - | |
| 125 | + | |
129 | 126 | | |
130 | 127 | | |
131 | 128 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
79 | 83 | | |
80 | 84 | | |
81 | 85 | | |
82 | | - | |
| 86 | + | |
| 87 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| 25 | + | |
24 | 26 | | |
25 | 27 | | |
26 | 28 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
0 commit comments