lutaml-store 0.2.4 → 0.3.1
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.
- checksums.yaml +4 -4
- data/CLAUDE.md +6 -0
- data/README.adoc +100 -3
- data/docs/astro.config.mjs +12 -0
- data/docs/package-lock.json +6098 -0
- data/docs/package.json +18 -0
- data/docs/public/favicon.svg +1 -0
- data/docs/public/lutaml-logo_logo-full-dark.svg +1 -0
- data/docs/public/lutaml-logo_logo-full-light.svg +1 -0
- data/docs/src/layouts/Base.astro +44 -0
- data/docs/src/layouts/Docs.astro +41 -0
- data/docs/src/pages/adapters.md +46 -0
- data/docs/src/pages/architecture.md +68 -0
- data/docs/src/pages/cloud-contract.md +48 -0
- data/docs/src/pages/formats.md +54 -0
- data/docs/src/pages/http-cache.md +43 -0
- data/docs/src/pages/index.md +82 -0
- data/docs/src/pages/quick-start.md +109 -0
- data/docs/src/pages/sources.md +83 -0
- data/docs/src/pages/stores.md +110 -0
- data/docs/src/styles/global.css +10 -0
- data/lib/lutaml/store/adapter/base.rb +14 -1
- data/lib/lutaml/store/adapter/filesystem.rb +283 -108
- data/lib/lutaml/store/adapter/memory.rb +16 -1
- data/lib/lutaml/store/adapter/sqlite.rb +26 -1
- data/lib/lutaml/store/basic_store.rb +11 -0
- data/lib/lutaml/store/cache_store.rb +104 -71
- data/lib/lutaml/store/config.rb +13 -1
- data/lib/lutaml/store/format.rb +13 -0
- data/lib/lutaml/store/manifest.rb +85 -0
- data/lib/lutaml/store/mirror.rb +94 -0
- data/lib/lutaml/store/repository.rb +178 -0
- data/lib/lutaml/store/source/base.rb +137 -0
- data/lib/lutaml/store/source/directory.rb +49 -0
- data/lib/lutaml/store/source/https.rb +109 -0
- data/lib/lutaml/store/source/rest.rb +61 -0
- data/lib/lutaml/store/source/zip.rb +52 -0
- data/lib/lutaml/store/source.rb +55 -0
- data/lib/lutaml/store/version.rb +1 -1
- data/lib/lutaml/store.rb +5 -0
- metadata +29 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4481af10b7c80db97bffbe2e1ad5119d615370596c46c88c84e536411d1ffdda
|
|
4
|
+
data.tar.gz: 618ad367561533d0aa576ef713dfb384173e34ec1ec0047e3cbab400af9c4dce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e22cb07d0781f6aba2c2ebc043322e5a756519284740b74661906b6f00cbce8536291967868b86fd2a1908de46e6c1d4450f35c517d4919f73020d65890dc63
|
|
7
|
+
data.tar.gz: 26f2ef6b87a85e2c020571319640ceed71955ace41c97c2052857b97bd8d68e599cf466cd349f31c802a0fda7172cd857ce1bfbed952076b7425aea07c7d2de0
|
data/CLAUDE.md
CHANGED
|
@@ -49,6 +49,12 @@ All inherit from `Format::Base`. Six formats: `Yaml`, `Yamls`, `Json`, `Jsonl`,
|
|
|
49
49
|
|
|
50
50
|
All inherit from `Adapter::Base`. Three backends: `Memory`, `FileSystem`, `SQLite`. Registered in `Adapter` module and resolved via `Adapter.resolve(:type, options)`. New adapters can be added with `Adapter.register(:custom, CustomClass)` without modifying existing code (OCP).
|
|
51
51
|
|
|
52
|
+
`Base#each_key` defaults to `keys.each`; `Base#update(key) { |old| new }` defaults to get + set inside `transaction`. Memory, FileSystem and Sqlite override `update` to be atomic. `FileSystem` percent-encodes file names (digest + key in `.meta` for long names), writes non-String values as JSON (`format: "json"` in `.meta`), and locks `<root>/.lock` (`with_lock(:ex/:sh)`, re-entrant per thread; a shared lock cannot be upgraded).
|
|
53
|
+
|
|
54
|
+
Inside `Lutaml::Store`, a bare `Monitor` resolves to `Lutaml::Store::Monitor` (the stats collector) — write `::Monitor` for Ruby's re-entrant lock.
|
|
55
|
+
|
|
56
|
+
Specs that load `sqlite3` must sort after `autoload_spec.rb` (see `sqlite_scale_spec.rb`). Run specs with a UTF-8 locale (`LANG=C.UTF-8`); the anti-pattern guard fails under US-ASCII.
|
|
57
|
+
|
|
52
58
|
### PackageStore and transports
|
|
53
59
|
|
|
54
60
|
`PackageStore` provides structured multi-model persistence. `PackageDefinition` declares which models, assets, and metadata the package contains. Transports (`DirectoryTransport`, `ZipTransport`) handle reading/writing to disk. Format handlers determine serialization per model entry.
|
data/README.adoc
CHANGED
|
@@ -17,8 +17,64 @@ It offers a unified interface for storing and retrieving complex model
|
|
|
17
17
|
hierarchies, batch file I/O with multiple serialization formats, HTTP-aware
|
|
18
18
|
caching, and package-based persistence with ZIP and directory transports.
|
|
19
19
|
|
|
20
|
+
It also provides **read-only Sources** for working with data repositories that
|
|
21
|
+
live outside your process — GitHub Pages, any static host, a REST API, a local
|
|
22
|
+
package directory, or a `.zip` distribution — through one facade that never
|
|
23
|
+
guesses which layer answered.
|
|
24
|
+
|
|
25
|
+
== Quick start: read a LutaML data repository
|
|
26
|
+
|
|
27
|
+
The same read API whether the data lives in the cloud, a local package, or a
|
|
28
|
+
downloaded `.zip` — backends are configured, never guessed:
|
|
29
|
+
|
|
30
|
+
[source,ruby]
|
|
31
|
+
----
|
|
32
|
+
require "lutaml/store"
|
|
33
|
+
|
|
34
|
+
# From a cloud API (api.relaton.org is the reference implementation)
|
|
35
|
+
repo = Lutaml::Store::Repository.new(
|
|
36
|
+
source: Lutaml::Store::Source.for(:rest,
|
|
37
|
+
base_url: "https://api.relaton.org", collection: "ietf"),
|
|
38
|
+
cache: Lutaml::Store::Source.for(:directory, path: "~/.cache/relaton/ietf"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# From a local GCR-style package — identical read API
|
|
42
|
+
repo = Lutaml::Store::Repository.new(
|
|
43
|
+
source: Lutaml::Store::Source.for(:directory, path: "~/gcr/relaton/ietf"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# From a downloaded .zip distribution — same API
|
|
47
|
+
repo = Lutaml::Store::Repository.new(
|
|
48
|
+
source: Lutaml::Store::Source.for(:zip, path: "ietf.zip"),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Read
|
|
52
|
+
repo.read("RFC 7231") # => bytes
|
|
53
|
+
repo.exist?("RFC 7231") # => true
|
|
54
|
+
repo.keys # => ["RFC 7231", ...]
|
|
55
|
+
repo.manifest # => Lutaml::Store::Manifest
|
|
56
|
+
|
|
57
|
+
# Parse into your model — the model class is declared, never inferred
|
|
58
|
+
repo.get("RFC 7231", MyModel)
|
|
59
|
+
|
|
60
|
+
# Search by metadata fields
|
|
61
|
+
repo.search(docid: "RFC 7231")
|
|
62
|
+
|
|
63
|
+
# Pull the whole collection into a local GCR-style package
|
|
64
|
+
repo.pull!(into: "~/gcr/relaton", collection: "ietf")
|
|
65
|
+
|
|
66
|
+
# Offline mode: never touch the source, serve from the local package only
|
|
67
|
+
offline = Lutaml::Store::Repository.new(source: repo.source, cache: repo.cache, mode: :offline)
|
|
68
|
+
offline.read("RFC 7231") # served from the cache, no network
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
20
72
|
== Features
|
|
21
73
|
|
|
74
|
+
* **Repository** — the uniform facade: one read API over any source (cloud, local, zip), explicit cache, online/offline modes
|
|
75
|
+
* **Source** (read-only) — Directory, Zip, Https (ETag/304), Rest (the lutaml cloud store API); 404 = NotFoundError, transport = BackendError
|
|
76
|
+
* **Manifest** — the enumeration SSOT: keys, sha256 digests, opaque shard metadata
|
|
77
|
+
* **Mirror** — pull any source into a GCR-style package (incremental, digest-verified); pack to a distributable .zip
|
|
22
78
|
* **DatabaseStore** — high-level CRUD with model registry, polymorphism, composites
|
|
23
79
|
* **PackageStore** — structured multi-model packages with ZIP and directory transport
|
|
24
80
|
* **BasicStore** — low-level key-value store with optional cache, events, monitoring
|
|
@@ -172,7 +228,21 @@ with optional caching, monitoring, and event emission.
|
|
|
172
228
|
=== Storage adapters
|
|
173
229
|
|
|
174
230
|
All adapters inherit from `Adapter::Base` and provide `get`, `set`, `delete`,
|
|
175
|
-
`exists?`, `keys`, `all`, `clear`, `size`, `each_key`, and bulk
|
|
231
|
+
`exists?`, `keys`, `all`, `clear`, `size`, `each_key`, `update`, and bulk
|
|
232
|
+
operations. `each_key` defaults to `keys.each`, so a custom adapter only needs
|
|
233
|
+
`keys`.
|
|
234
|
+
|
|
235
|
+
`update(key) { |old| new }` is a read-modify-write: the block gets the current
|
|
236
|
+
value (`nil` when the key is missing) and its result is stored. It is atomic
|
|
237
|
+
across threads for Memory, and across threads and processes for FileSystem and
|
|
238
|
+
SQLite. `BasicStore#update` calls it and refreshes the read cache.
|
|
239
|
+
|
|
240
|
+
[source,ruby]
|
|
241
|
+
----
|
|
242
|
+
store = Lutaml::Store::BasicStore.new(adapter_type: :filesystem,
|
|
243
|
+
adapter_options: { path: "./data" })
|
|
244
|
+
store.update("counter") { |n| (n.to_i + 1).to_s }
|
|
245
|
+
----
|
|
176
246
|
|
|
177
247
|
[cols="1,1,3"]
|
|
178
248
|
|===
|
|
@@ -585,6 +655,21 @@ store = Lutaml::Store.new(
|
|
|
585
655
|
Persistent file-based storage with SHA-256 integrity checks. Files organized
|
|
586
656
|
by key in subdirectories.
|
|
587
657
|
|
|
658
|
+
* *File names.* Every byte outside `[A-Za-z0-9._-]` is percent-encoded, so
|
|
659
|
+
`ISO/19115` and `ISO:19115` get different files and `keys` returns the
|
|
660
|
+
original keys. A leading `.` is encoded too, so no file is hidden or outside
|
|
661
|
+
the root. A name longer than 200 bytes becomes `~` plus the SHA-256 of the
|
|
662
|
+
key; the `.meta` file beside it keeps the key. Files written by 0.3.0 (with
|
|
663
|
+
`_` in place of unsafe characters) are still read, and move to the new name
|
|
664
|
+
on the next write of that key.
|
|
665
|
+
* *Values.* A `String` is written as is. Any other value (for example the
|
|
666
|
+
Hash that `DatabaseStore` saves) is written as JSON and read back as JSON.
|
|
667
|
+
* *Locks.* Writes hold an exclusive `flock` on `<path>/.lock` and reads hold a
|
|
668
|
+
shared one. Data and `.meta` files are written to a unique temp file and
|
|
669
|
+
renamed into place, so readers never see a partial write.
|
|
670
|
+
* *Case-insensitive file systems* (the macOS default) can still merge keys that
|
|
671
|
+
differ only in letter case.
|
|
672
|
+
|
|
588
673
|
=== SQLite
|
|
589
674
|
|
|
590
675
|
[source,ruby]
|
|
@@ -625,8 +710,20 @@ Error hierarchy:
|
|
|
625
710
|
|
|
626
711
|
== Thread safety
|
|
627
712
|
|
|
628
|
-
|
|
629
|
-
|
|
713
|
+
* `Adapter::Memory`: writes are synchronized with a Monitor; reads use a
|
|
714
|
+
frozen snapshot.
|
|
715
|
+
* `Adapter::FileSystem`: safe across threads and processes that share one
|
|
716
|
+
directory (see the FileSystem section above). `transaction { ... }` holds the exclusive
|
|
717
|
+
lock for the whole block.
|
|
718
|
+
* `Adapter::Sqlite`: SQLite locking with a busy timeout; `update` uses
|
|
719
|
+
`BEGIN IMMEDIATE`.
|
|
720
|
+
* `CacheStore`: all operations run under one lock per instance. The TTL and
|
|
721
|
+
LRU bookkeeping is per process.
|
|
722
|
+
* `BasicStore`: the optional read cache is per process. Disable it
|
|
723
|
+
(`cache: { enabled: false }`) when other processes write the same store.
|
|
724
|
+
|
|
725
|
+
A `get` followed by a `set` is never atomic. Use `update` for a
|
|
726
|
+
read-modify-write.
|
|
630
727
|
|
|
631
728
|
== Development
|
|
632
729
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from "astro/config";
|
|
2
|
+
import vue from "@astrojs/vue";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
site: "https://lutaml.github.io",
|
|
7
|
+
base: "/lutaml-store",
|
|
8
|
+
integrations: [vue()],
|
|
9
|
+
vite: {
|
|
10
|
+
plugins: [tailwindcss()],
|
|
11
|
+
},
|
|
12
|
+
});
|