lepus-saga-sequel 0.3.0
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 +7 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +173 -0
- data/Rakefile +8 -0
- data/docs/brand/BRAND.md +67 -0
- data/docs/brand/apple-touch-icon.png +0 -0
- data/docs/brand/favicon-16.png +0 -0
- data/docs/brand/favicon-32.png +0 -0
- data/docs/brand/favicon.ico +0 -0
- data/docs/brand/favicon.svg +1 -0
- data/docs/brand/icon-512.png +0 -0
- data/docs/brand/lockup-lepus-mono.svg +1 -0
- data/docs/brand/lockup-lepus-on-dark.svg +1 -0
- data/docs/brand/lockup-lepus-on-light.svg +1 -0
- data/docs/brand/lockup-lepus-saga-mono.svg +1 -0
- data/docs/brand/lockup-lepus-saga-on-dark.svg +1 -0
- data/docs/brand/lockup-lepus-saga-on-light.svg +1 -0
- data/docs/brand/lockup-lepus-saga.svg +1 -0
- data/docs/brand/lockup-lepus.svg +1 -0
- data/docs/brand/mark-mono.svg +1 -0
- data/docs/brand/mark-on-dark.svg +1 -0
- data/docs/brand/mark-on-light.svg +1 -0
- data/docs/brand/mark-small-mono.svg +1 -0
- data/docs/brand/mark-small-on-dark.svg +1 -0
- data/docs/brand/mark-small-on-light.svg +1 -0
- data/docs/brand/mark-small.svg +1 -0
- data/docs/brand/mark.svg +1 -0
- data/lib/lepus/saga/sequel/execution.rb +72 -0
- data/lib/lepus/saga/sequel/migrations/001_create_lepus_saga_tables.rb +62 -0
- data/lib/lepus/saga/sequel/migrations/002_add_lepus_saga_dashboard_indexes.rb +31 -0
- data/lib/lepus/saga/sequel/record.rb +34 -0
- data/lib/lepus/saga/sequel/step_attempt.rb +35 -0
- data/lib/lepus/saga/sequel/version.rb +9 -0
- data/lib/lepus/saga/sequel.rb +45 -0
- data/lib/lepus/saga/stores/sequel.rb +266 -0
- data/mise.toml +2 -0
- metadata +207 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 688935ea658850abc331020a8bef9b9a11a940c99e19a65c054fb593871406b9
|
|
4
|
+
data.tar.gz: 69feb77e326f97a3a344c29b79f6ebb80f254906239a7d3ff99a70bbd38df562
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 36b993b968e9781e5eb1cedc84bb52fa2329bfa5b48f6e81b9f941cf8a6b3b46890108eaa68d758bd70451101c69c82969f03cdf52e51622d02292acc5eeacf1
|
|
7
|
+
data.tar.gz: df9f85d83bdc581a1db3da45616a19e2c8b2f4fedcd636bff5322aae401f8505f062811fb71b58b84e6498f842754b0100af1720a0d312adaaa804dd15dced45
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## 0.3.0 - Unreleased
|
|
8
|
+
* Added: brand mark and README logo
|
|
9
|
+
* Added: `Lepus::Saga::Stores::Sequel`, selected with `Lepus::Saga.configure { |c| c.store = :sequel }`, and `Lepus::Saga::Sequel.db=` to bind the models to a database other than `Sequel::Model.db`
|
|
10
|
+
* Added: `Lepus::Saga::Sequel::Execution` and `StepAttempt` models (UUIDv7 ids, json context, read datasets) on the `saga_executions` and `saga_step_attempts` tables
|
|
11
|
+
* Added: `Lepus::Saga::Sequel.migrate(db)` runs the bundled migrations, tracked in `lepus_saga_schema_info`; the schema and index names match the lepus-saga-active_record install migration, `failure_reason` is `text` and there is no `lock_version` column
|
|
12
|
+
* Added: migration 002 adds the dashboard indexes to tables created without them
|
|
13
|
+
* Added: `Execution.prune(finished_before:)` deletes completed executions and their step attempts in batches
|
|
14
|
+
* Added: the optional query methods of the store contract (`list_executions`, `count_executions`, `execution_series`, `step_latency`, `failure_summary`, `saga_names`) in portable SQL, with time buckets and percentiles computed in Ruby over at most `max_rows` rows; `queries?` is true
|
|
15
|
+
* Added: SQLite locks open `IMMEDIATE` transactions, since SQLite has no `FOR UPDATE`
|
data/Gemfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gemspec
|
|
6
|
+
|
|
7
|
+
core = ENV.fetch("LEPUS_SAGA_PATH", File.expand_path("../lepus-saga", __dir__))
|
|
8
|
+
if File.directory?(core)
|
|
9
|
+
gem "lepus-saga", path: core
|
|
10
|
+
else
|
|
11
|
+
gem "lepus-saga", github: "marcosgz/lepus-saga"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
gem "sqlite3", ">= 2.1"
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcos G. Zimmermann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/brand/lockup-lepus-saga-on-dark.svg">
|
|
4
|
+
<img alt="Lepus saga" src="docs/brand/lockup-lepus-saga-on-light.svg" width="359">
|
|
5
|
+
</picture>
|
|
6
|
+
</h1>
|
|
7
|
+
|
|
8
|
+
<p align="center"><code>lepus-saga-sequel</code></p>
|
|
9
|
+
|
|
10
|
+
[Sequel](https://sequel.jeremyevans.net) store for [lepus-saga](https://github.com/marcosgz/lepus-saga). Executions and step attempts live in the `saga_executions` and `saga_step_attempts` tables. Works with PostgreSQL, MySQL/MariaDB and SQLite.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "lepus-saga"
|
|
16
|
+
gem "lepus-saga-sequel"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The gem ships its migrations. Run them with:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "lepus/saga/sequel"
|
|
23
|
+
|
|
24
|
+
Lepus::Saga::Sequel.migrate(DB) # create the tables
|
|
25
|
+
Lepus::Saga::Sequel.migrate(DB, target: 0) # drop them
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The version is tracked in its own `lepus_saga_schema_info` table, so it never collides with your app's `schema_info` or `schema_migrations`. To keep everything in one migration history instead, copy the files from `lib/lepus/saga/sequel/migrations` into your own migrations directory.
|
|
29
|
+
|
|
30
|
+
Ids are UUIDv7 strings generated in Ruby, the context columns are `json` and `failure_reason` is `text` (a step's error message can outgrow `VARCHAR(255)`, and MySQL/MariaDB strict mode would reject the write that fails the saga). The table, column and index names are the ones the lepus-saga-active_record generator creates, so an app can move between the two gems on the same tables. Column types can differ slightly: Sequel creates unsized strings as `text` on PostgreSQL and `duration_ms` as a double-precision float on MySQL/MariaDB.
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Sequel.database_timezone = :utc
|
|
36
|
+
DB = Sequel.connect(ENV["DATABASE_URL"])
|
|
37
|
+
|
|
38
|
+
Lepus::Saga.configure do |config|
|
|
39
|
+
config.store = :sequel
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`:sequel` requires `lepus/saga/stores/sequel` and instantiates `Lepus::Saga::Stores::Sequel`. The models use `Sequel::Model.db` (the first database you connected to). To keep the saga tables in another database, set it before the models are first used:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
Lepus::Saga::Sequel.db = SAGA_DB
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To use your own subclasses of the models (another table name, extra plugins), redeclare the associations so `step_attempts`, `attempts_for_current_step` and `prune` use your attempt class, and pass an instance:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class SagaExecution < Lepus::Saga::Sequel::Execution
|
|
53
|
+
set_dataset :order_saga_executions
|
|
54
|
+
one_to_many :step_attempts, class: "SagaStepAttempt", key: :saga_execution_id
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class SagaStepAttempt < Lepus::Saga::Sequel::StepAttempt
|
|
58
|
+
set_dataset :order_saga_step_attempts
|
|
59
|
+
many_to_one :execution, class: "SagaExecution", key: :saga_execution_id
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
config.store = Lepus::Saga::Stores::Sequel.new(execution_class: SagaExecution, step_attempt_class: SagaStepAttempt)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The rest of the setup (definitions, consumers, operations, instrumentation) is in the [lepus-saga README](https://github.com/marcosgz/lepus-saga#readme).
|
|
66
|
+
|
|
67
|
+
### Requirements
|
|
68
|
+
|
|
69
|
+
- **Times in UTC.** The engine writes `Time.now.utc` and compares it with stored times to find stalled executions. Sequel stores times in local time by default, which mixes the two in a `timestamp`/`datetime` column. Set `Sequel.database_timezone = :utc` (or `DB.timezone = :utc` for one database).
|
|
70
|
+
- **Fractional seconds on MySQL/MariaDB.** Open the database with `fractional_seconds: true` (or `?fractional_seconds=true` in the URL) *before* running the migrations. Without it Sequel creates `datetime` columns without microseconds and truncates the times it writes, so executions created in the same second sort arbitrarily.
|
|
71
|
+
- **A busy handler on SQLite with threads.** SQLite serializes writers on the whole database. Wait for the lock with `busy_handler_timeout`, which lets other Ruby threads run while it waits (`busy_timeout`, which Sequel's `:timeout` option sets, does not):
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
DB = Sequel.sqlite("db/app.sqlite3", after_connect: ->(connection) { connection.busy_handler_timeout = 10_000 })
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Models
|
|
78
|
+
|
|
79
|
+
`Lepus::Saga::Sequel::Execution` and `Lepus::Saga::Sequel::StepAttempt` are `Sequel::Model` classes that include the core's `ExecutionBehaviour` and `StepAttemptBehaviour`, so they answer `running?`, `terminal?`, `definition_class`, `partition` and the rest.
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Lepus::Saga::Sequel::Execution.for_resource("Order", order.id).completed.empty?
|
|
83
|
+
Lepus::Saga::Sequel::Execution.in_progress.where(partition: "priority")
|
|
84
|
+
Lepus::Saga::Sequel::Execution.recent_first.limit(10)
|
|
85
|
+
Lepus::Saga::Sequel::Execution.stalled(15 * 60)
|
|
86
|
+
execution.step_attempts_dataset.chronological
|
|
87
|
+
execution.attempts_for_current_step
|
|
88
|
+
Lepus::Saga::Sequel::Execution::STATUSES
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Defaults (status, direction, epoch, resume count, context, progress clock) are set in `before_validation`, and the JSON columns are serialized there too, so always save through `save`, never with `validate: false`. Saves raise `Sequel::ValidationFailed` on invalid records whatever `raise_on_save_failure` says elsewhere in the app.
|
|
92
|
+
|
|
93
|
+
Nothing deletes old rows on its own. Prune completed executions (and their step attempts) from a recurring job; call it on a filtered dataset to keep some:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
Lepus::Saga::Sequel::Execution.prune(finished_before: Time.now.utc - 30 * 86_400)
|
|
97
|
+
Lepus::Saga::Sequel::Execution.exclude(id: pinned_ids).prune(finished_before: Time.now.utc - 30 * 86_400)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## How it maps the store contract
|
|
101
|
+
|
|
102
|
+
| Store operation | Sequel |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `with_lock(execution)` | `transaction { execution.lock! }` (reload in place with `SELECT ... FOR UPDATE`) |
|
|
105
|
+
| `with_locked_execution(id)` | `transaction { Execution.for_update.first(id:) }`, one query |
|
|
106
|
+
| `save_execution`, `save_step_attempt` | `save`, which writes every column, so a context changed in place is persisted |
|
|
107
|
+
| `increment_resume_count` | `UPDATE ... SET resume_count = resume_count + 1, updated_at = ?`, then reads the stored value back |
|
|
108
|
+
| `create_step_attempt` | `create`; `Sequel::UniqueConstraintViolation` from `idx_saga_step_attempts_dedup` becomes `Lepus::Saga::DuplicateAttemptError` |
|
|
109
|
+
| `stalled_executions(before:)` | `in_progress.where(last_progress_at < before).order(:last_progress_at).all` |
|
|
110
|
+
| attempt queries | `where(saga_execution_id:, step_name:, direction:, epoch:)` plus `count`/`empty?` |
|
|
111
|
+
| `list_executions` | Filters on the executions table, `order(Sequel.desc(:id))` with `id < before_id` for keyset paging; the resource search is `ILIKE` with the LIKE wildcards escaped; the error filters are `id IN (SELECT saga_execution_id FROM saga_step_attempts WHERE status = 'failed' ...)` |
|
|
112
|
+
| `count_executions` | `group_and_count(...)`; grouping by partition also groups by saga name to resolve executions without one |
|
|
113
|
+
| `execution_series` | Selects `created_at`, and `finished_at` and `status` of terminal executions, newest first and at most `max_rows` + 1; buckets them in Ruby |
|
|
114
|
+
| `step_latency` | Joins attempts to executions and selects the newest `max_rows` + 1 durations; percentiles in Ruby |
|
|
115
|
+
| `failure_summary` | `GROUP BY error_class, saga_name, step_name` with `COUNT(*)` and `COUNT(DISTINCT saga_execution_id)`, ordered by `COUNT(*)` and `MAX(finished_at)`, then one query per group for its latest failure |
|
|
116
|
+
| `saga_names` | `distinct.order(:saga_name).select_map(:saga_name)` |
|
|
117
|
+
|
|
118
|
+
The query methods (the optional part of the contract, used by dashboards) stick to SQL every supported database runs the same way: no `date_trunc`, no `percentile_cont`.
|
|
119
|
+
|
|
120
|
+
The methods that return several records return Arrays: the engine runs other queries while it walks them, which Sequel's streaming `each` does not allow.
|
|
121
|
+
|
|
122
|
+
On SQLite, which has no `FOR UPDATE`, the store opens its lock transactions as `IMMEDIATE`: the write lock is taken up front, so a second locker waits instead of reading alongside, and writers are serialized on the whole database instead of one row.
|
|
123
|
+
|
|
124
|
+
### No optimistic locking
|
|
125
|
+
|
|
126
|
+
The engine's concurrency control is the row lock plus the epoch fence: every change to an execution happens under `with_lock`, and the one write made without it, the healer's `resume_count` bump, is a single atomic `UPDATE`. Optimistic locking would add nothing, so the models do not load Sequel's `optimistic_locking` plugin and the migration creates no `lock_version` column.
|
|
127
|
+
|
|
128
|
+
The `many_to_one :execution` of a step attempt is not validated, because the foreign key already guards it and validating it would load the execution again on every attempt the executor opens under the lock.
|
|
129
|
+
|
|
130
|
+
## Existing tables
|
|
131
|
+
|
|
132
|
+
The first migration uses `create_table?`, so it adopts tables that already exist (created by hand or by the lepus-saga-active_record generator). Note that rolling it back drops them.
|
|
133
|
+
|
|
134
|
+
Tables created before the indexes the query methods filter on existed lack them. The second migration adds `saga_executions (saga_name, created_at)`, `(status, created_at)` and `(finished_at)`, and `saga_step_attempts (started_at)` and `(error_class, started_at)`, skipping any that already exist.
|
|
135
|
+
|
|
136
|
+
Tables created by hand (PostgreSQL `uuid` ids, `jsonb` context) work too: the models set UUIDv7 ids before insert and read the JSON columns whether the driver returns a String or a parsed value (the `pg_json` extension).
|
|
137
|
+
|
|
138
|
+
## Architecture
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
lepus-saga lepus-saga-sequel
|
|
142
|
+
────────── ─────────────────
|
|
143
|
+
Lepus::Saga::Stores::Base <─── Lepus::Saga::Stores::Sequel
|
|
144
|
+
ExecutionBehaviour <─── Lepus::Saga::Sequel::Execution
|
|
145
|
+
StepAttemptBehaviour <─── Lepus::Saga::Sequel::StepAttempt
|
|
146
|
+
"a lepus saga store" ───> spec/lepus/saga/stores/sequel_spec.rb
|
|
147
|
+
"a lepus saga engine" ───> (contract and full engine on every database)
|
|
148
|
+
"a lepus saga query store" ───>
|
|
149
|
+
lib/lepus/saga/sequel/migrations
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bin/setup
|
|
156
|
+
bundle exec rspec # SQLite file in tmp/
|
|
157
|
+
bundle exec rubocop
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Against PostgreSQL and MariaDB, start the databases with `docker compose up -d` and run:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5433/lepus_saga_test BUNDLE_GEMFILE=ci/Gemfile.postgresql bundle exec rspec
|
|
164
|
+
DATABASE_URL="mysql2://root:root@127.0.0.1:3317/lepus_saga_test?fractional_seconds=true" BUNDLE_GEMFILE=ci/Gemfile.mariadb bundle exec rspec
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Examples tagged `:row_locks` (one row locked while another stays free, a single `FOR UPDATE` query) are skipped on SQLite.
|
|
168
|
+
|
|
169
|
+
The Gemfile uses `../lepus-saga` when it exists (or `LEPUS_SAGA_PATH`), and the GitHub repository otherwise.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/docs/brand/BRAND.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Lepus brand kit
|
|
2
|
+
|
|
3
|
+
## The mark
|
|
4
|
+
|
|
5
|
+
A capital L drawn as a single route. The upright is the hare's ear; the foot curls back into a rounded body, and the amber dot inside it is a message in transit. One line, one stop, one message: the mark reads as "L", as a hare, and as a route carrying work.
|
|
6
|
+
|
|
7
|
+
## Colours
|
|
8
|
+
|
|
9
|
+
| Role | Light background | Dark background |
|
|
10
|
+
|-------|------------------|-----------------|
|
|
11
|
+
| Ink | `#1a2233` | `#e3e9f3` |
|
|
12
|
+
| Amber | `#f2a30f` | `#f2a30f` |
|
|
13
|
+
| Tile (app icons) | `#1a2233` | `#1a2233` |
|
|
14
|
+
|
|
15
|
+
Amber is reserved for the message dot and the rule under "SAGA". Do not recolour the line in amber or put the dot in ink (except in the mono files).
|
|
16
|
+
|
|
17
|
+
## Clear space and minimum size
|
|
18
|
+
|
|
19
|
+
- Clear space: keep at least the height of the mark's body (the rounded loop, about one third of the mark's height) empty on every side.
|
|
20
|
+
- `mark*.svg`: 48 px and up.
|
|
21
|
+
- `mark-small*.svg` / `favicon.svg`: 16 to 48 px. Its line is heavier and every edge sits on the 16, 32, 48 and 64 px pixel grids, so it stays sharp at favicon sizes. Do not use it large.
|
|
22
|
+
- `lockup-lepus*`: 120 px wide minimum. `lockup-lepus-saga*`: 180 px wide minimum (below that, "SAGA" gets too small; use `lockup-lepus` or the mark alone).
|
|
23
|
+
|
|
24
|
+
## Which file where
|
|
25
|
+
|
|
26
|
+
| Use | File |
|
|
27
|
+
|-----|------|
|
|
28
|
+
| README header, Lepus | `lockup-lepus-on-light.svg` + `lockup-lepus-on-dark.svg` in a `<picture>` (below) |
|
|
29
|
+
| README header, Lepus saga | `lockup-lepus-saga-on-light.svg` + `lockup-lepus-saga-on-dark.svg` |
|
|
30
|
+
| Web page header that follows the OS theme | `lockup-lepus.svg`, `lockup-lepus-saga.svg`, `mark.svg` (ink switches with `prefers-color-scheme`) |
|
|
31
|
+
| Inline in an app that sets its own text colour | `*-mono.svg` (everything is `currentColor`) |
|
|
32
|
+
| Fixed background you control | `*-on-light.svg` / `*-on-dark.svg` |
|
|
33
|
+
| Browser tab | `favicon.svg` (adaptive), with `favicon.ico` (16/32/48) as fallback |
|
|
34
|
+
| iOS home screen | `apple-touch-icon.png` (180, solid tile) |
|
|
35
|
+
| Web manifest / PWA | `icon-512.png` (solid tile, mark inside the maskable safe zone) |
|
|
36
|
+
| Legacy PNG favicons | `favicon-16.png`, `favicon-32.png` (transparent, light-background ink) |
|
|
37
|
+
|
|
38
|
+
All wordmark text is outlined Overpass (SIL Open Font License), so no font is needed to render it.
|
|
39
|
+
|
|
40
|
+
### README
|
|
41
|
+
|
|
42
|
+
GitHub follows the viewer's GitHub theme only through `<picture>`, so use the light/dark pair rather than the adaptive file:
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
<p align="center">
|
|
46
|
+
<picture>
|
|
47
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/brand/lockup-lepus-on-dark.svg">
|
|
48
|
+
<img alt="Lepus" src="docs/brand/lockup-lepus-on-light.svg" width="278">
|
|
49
|
+
</picture>
|
|
50
|
+
</p>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For Lepus saga, swap in `lockup-lepus-saga-on-dark.svg` / `lockup-lepus-saga-on-light.svg` and `width="359"`.
|
|
54
|
+
|
|
55
|
+
### HTML head
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
|
59
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
60
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Don'ts
|
|
64
|
+
|
|
65
|
+
- Don't redraw the wordmark in live text or another typeface.
|
|
66
|
+
- Don't stretch, rotate, outline or add effects to the mark.
|
|
67
|
+
- Don't put the ink mark on mid-tone or busy backgrounds; use the tile or the mono file instead.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><path class="i" d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="currentColor"/></g><path fill="currentColor" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#e3e9f3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#e3e9f3" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#1a2233" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#1a2233" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="currentColor"/></g><path fill="currentColor" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path fill="currentColor" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#e3e9f3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#e3e9f3" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path fill="#e3e9f3" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><g transform="translate(-13.03 -60.26) scale(1.2414)"><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#1a2233" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path fill="#1a2233" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path fill="#1a2233" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 372.21 84.00" width="359" height="81" role="img" aria-label="Lepus saga"><title>Lepus saga</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><g transform="translate(-13.03 -60.26) scale(1.2414)"><path class="i" d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path class="f" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/><path class="f" d="M272 0.4Q269.87 0.4 268.13 -0.34Q266.4 -1.07 265.17 -2.4Q263.94 -3.73 263.35 -5.5L266.86 -6.8Q267.59 -5.22 268.96 -4.22Q270.33 -3.22 272.09 -3.22Q273.97 -3.22 275.1 -4.06Q276.22 -4.89 276.22 -6.48Q276.22 -7.58 275.55 -8.33Q274.89 -9.07 273.78 -9.59Q272.67 -10.12 271.33 -10.58Q270.08 -11.03 268.81 -11.56Q267.53 -12.1 266.48 -12.87Q265.43 -13.64 264.79 -14.78Q264.15 -15.93 264.15 -17.58Q264.15 -19.24 265.05 -20.63Q265.94 -22.02 267.64 -22.86Q269.33 -23.7 271.73 -23.7Q273.71 -23.7 275.22 -23.05Q276.74 -22.41 277.73 -21.31Q278.71 -20.21 279.1 -18.82L275.64 -17.66Q275.19 -18.69 274.19 -19.39Q273.18 -20.09 271.51 -20.09Q269.87 -20.09 268.96 -19.42Q268.04 -18.75 268.04 -17.54Q268.04 -16.68 268.66 -16.09Q269.27 -15.5 270.31 -15.04Q271.34 -14.59 272.6 -14.14Q273.93 -13.66 275.26 -13.08Q276.58 -12.49 277.68 -11.64Q278.78 -10.79 279.45 -9.55Q280.11 -8.3 280.11 -6.51Q280.11 -4.31 279.04 -2.77Q277.97 -1.23 276.14 -0.41Q274.3 0.4 272 0.4ZM286.33 0 294.99 -23.3H298.62L307.3 0H303.22L301.33 -5.18H292.3L290.45 0H286.33ZM293.53 -8.67H300.07L297.52 -15.7Q297.41 -15.99 297.27 -16.38Q297.14 -16.76 297.01 -17.13Q296.88 -17.51 296.81 -17.8Q296.73 -17.51 296.61 -17.13Q296.48 -16.76 296.35 -16.38Q296.21 -15.99 296.11 -15.7L293.53 -8.67ZM323.19 0.4Q320.53 0.4 318.62 -0.61Q316.71 -1.61 315.5 -3.34Q314.29 -5.06 313.71 -7.21Q313.13 -9.37 313.13 -11.65Q313.13 -13.85 313.74 -15.99Q314.34 -18.12 315.57 -19.87Q316.8 -21.61 318.69 -22.65Q320.59 -23.7 323.19 -23.7Q325.4 -23.7 326.91 -23.03Q328.42 -22.37 329.43 -21.31Q330.45 -20.25 331.19 -19.07L327.79 -17.13Q327.01 -18.3 325.95 -19.17Q324.88 -20.05 323.19 -20.05Q321.46 -20.05 320.29 -19.28Q319.12 -18.51 318.43 -17.27Q317.73 -16.02 317.42 -14.54Q317.11 -13.07 317.11 -11.65Q317.11 -10.04 317.46 -8.54Q317.81 -7.04 318.54 -5.85Q319.26 -4.66 320.42 -3.96Q321.58 -3.27 323.19 -3.27Q324.49 -3.27 325.51 -3.84Q326.52 -4.41 327.1 -5.4Q327.68 -6.39 327.68 -7.64V-8.24H323.64V-11.85H331.66V-8.68Q331.66 -5.74 330.54 -3.71Q329.42 -1.69 327.5 -0.64Q325.58 0.4 323.19 0.4ZM338.01 0 346.68 -23.3H350.3L358.98 0H354.9L353.01 -5.18H343.99L342.13 0H338.01ZM345.21 -8.67H351.76L349.2 -15.7Q349.09 -15.99 348.96 -16.38Q348.82 -16.76 348.69 -17.13Q348.57 -17.51 348.49 -17.8Q348.41 -17.51 348.29 -17.13Q348.17 -16.76 348.03 -16.38Q347.9 -15.99 347.79 -15.7L345.21 -8.67Z"/><rect x="261.75" y="6.00" width="98.46" height="4.5" rx="2.25" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.00 -64.40 259.67 84.00" width="278" height="90" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><g transform="translate(-13.03 -60.26) scale(1.2414)"><path class="i" d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></g><path class="f" d="M80.05 0V-44.8H89.26V-8.91H110.81V0H80.05ZM128.16 0.77Q123.38 0.77 119.92 -1.31Q116.46 -3.39 114.61 -7.24Q112.76 -11.08 112.76 -16.38Q112.76 -21.64 114.65 -25.48Q116.54 -29.31 120 -31.39Q123.45 -33.47 128.16 -33.47Q132.23 -33.47 135.52 -31.8Q138.81 -30.12 140.75 -26.68Q142.69 -23.24 142.69 -17.97Q142.69 -17.1 142.66 -15.84Q142.62 -14.57 142.45 -13.31H121.38Q121.44 -11.31 122.29 -9.82Q123.13 -8.34 124.64 -7.52Q126.14 -6.71 128.19 -6.71Q130.6 -6.71 132.3 -7.49Q134 -8.28 135.56 -10.07L140.62 -4.94Q138.41 -2.35 135.33 -0.79Q132.26 0.77 128.16 0.77ZM121.47 -20.02H134.24Q134.04 -21.93 133.27 -23.27Q132.5 -24.6 131.17 -25.3Q129.85 -26 127.96 -26Q125.38 -26 123.59 -24.46Q121.8 -22.92 121.47 -20.02ZM148.64 12.99V-32.7H157.43V-30.41Q159.85 -33.47 164.23 -33.47Q168.44 -33.47 171.47 -31.4Q174.51 -29.34 176.15 -25.5Q177.78 -21.66 177.78 -16.33Q177.78 -11.21 176.07 -7.37Q174.35 -3.53 171.28 -1.38Q168.2 0.77 164.12 0.77Q161.97 0.77 160.28 0Q158.6 -0.76 157.43 -2.25V9.04L148.64 12.99ZM162.75 -7.69Q165.73 -7.69 167.35 -9.96Q168.96 -12.24 168.96 -16.33Q168.96 -20.51 167.35 -22.77Q165.74 -25.02 162.75 -25.02Q161 -25.02 159.72 -24.46Q158.44 -23.9 157.43 -22.71V-10.11Q158 -9.03 159.47 -8.36Q160.94 -7.69 162.75 -7.69ZM195.02 0.77Q189.74 0.77 186.71 -2.57Q183.68 -5.91 183.68 -11.78V-32.7H192.47V-15.4Q192.47 -11.26 193.74 -9.48Q195.02 -7.69 197.92 -7.69Q201.03 -7.69 202.46 -9.53Q203.89 -11.37 203.89 -15.27V-32.7H212.68V0H203.89V-3.03Q202.42 -1.17 200.15 -0.2Q197.87 0.77 195.02 0.77ZM231.57 0.77Q227.63 0.77 224.02 -0.91Q220.41 -2.59 218.35 -5.4L224 -10.18Q225.63 -8.5 227.77 -7.49Q229.9 -6.48 231.85 -6.48Q234.11 -6.48 235.33 -7.2Q236.55 -7.92 236.55 -9.25Q236.55 -10.07 235.97 -10.74Q235.39 -11.4 233.84 -12.14Q232.3 -12.88 229.42 -13.93Q223.86 -15.94 221.58 -18.28Q219.31 -20.63 219.31 -24.27Q219.31 -28.39 222.64 -30.93Q225.98 -33.47 231.34 -33.47Q234.98 -33.47 238.08 -32.09Q241.17 -30.71 243.42 -28.08L237.74 -23.4Q234.87 -26.23 231.17 -26.23Q229.41 -26.23 228.31 -25.58Q227.22 -24.94 227.22 -23.91Q227.22 -22.87 228.53 -22.03Q229.85 -21.2 233.5 -20.06Q237.33 -18.91 239.75 -17.4Q242.17 -15.9 243.31 -13.9Q244.44 -11.9 244.44 -9.28Q244.44 -4.58 241.01 -1.9Q237.57 0.77 231.57 0.77Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#e3e9f3" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke="#1a2233" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke="currentColor" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="currentColor"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke="#e3e9f3" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><path d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke="#1a2233" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><path class="i" d="M12 6 V40 A16 16 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V18" fill="none" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40" cy="46" r="5" fill="#f2a30f"/></svg>
|
data/docs/brand/mark.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="1 0 64 64" role="img" aria-label="Lepus"><title>Lepus</title><style>.i{stroke:#1a2233}.f{fill:#1a2233}@media (prefers-color-scheme:dark){.i{stroke:#e3e9f3}.f{fill:#e3e9f3}}</style><path class="i" d="M14 5 V42 A14 14 0 0 0 28 56 H42 A10 10 0 0 0 42 36 H28 V15" fill="none" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="40.5" cy="46" r="4" fill="#f2a30f"/></svg>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Saga
|
|
5
|
+
module Sequel
|
|
6
|
+
class Execution < ::Sequel::Model(Lepus::Saga::Sequel.db[:saga_executions])
|
|
7
|
+
include ExecutionBehaviour
|
|
8
|
+
|
|
9
|
+
plugin Record, :context
|
|
10
|
+
plugin :association_dependencies
|
|
11
|
+
|
|
12
|
+
one_to_many :step_attempts, class: "Lepus::Saga::Sequel::StepAttempt", key: :saga_execution_id
|
|
13
|
+
add_association_dependencies step_attempts: :delete
|
|
14
|
+
|
|
15
|
+
dataset_module do
|
|
16
|
+
where :in_progress, status: IN_PROGRESS_STATUSES
|
|
17
|
+
where :completed, status: "completed"
|
|
18
|
+
order :recent_first, ::Sequel.desc(:created_at)
|
|
19
|
+
|
|
20
|
+
def for_resource(type, id)
|
|
21
|
+
where(resource_type: type, resource_id: id.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Deliberately not `updated_at`: a re-publish, a lock or an attempt
|
|
25
|
+
# opened and abandoned all touch the row while the cursor stands
|
|
26
|
+
# still, so a wedged execution would keep resetting its own stall clock.
|
|
27
|
+
def stalled(older_than = 15 * 60)
|
|
28
|
+
in_progress.where(::Sequel[:last_progress_at] < Time.now.utc - older_than)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Deletes completed executions that finished before the cutoff, with
|
|
32
|
+
# their step attempts, batch by batch. Call it on a filtered dataset
|
|
33
|
+
# to keep some: `exclude(id: ids_to_keep).prune(finished_before:)`.
|
|
34
|
+
# Returns the number of executions deleted.
|
|
35
|
+
def prune(finished_before:, batch_size: 1_000)
|
|
36
|
+
step_attempt_class = model.association_reflection(:step_attempts).associated_class
|
|
37
|
+
expired = completed.where(::Sequel[:finished_at] < finished_before)
|
|
38
|
+
deleted = 0
|
|
39
|
+
until (ids = expired.limit(batch_size).select_map(:id)).empty?
|
|
40
|
+
db.transaction do
|
|
41
|
+
step_attempt_class.where(saga_execution_id: ids).delete
|
|
42
|
+
deleted += model.where(id: ids).delete
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
deleted
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def attempts_for_current_step
|
|
50
|
+
step_attempts_dataset.where(current_step_scope)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate
|
|
54
|
+
super
|
|
55
|
+
validates_presence %i[saga_name resource_type resource_id]
|
|
56
|
+
validates_includes STATUSES, :status
|
|
57
|
+
validates_includes DIRECTIONS, :direction
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def before_validation
|
|
61
|
+
self.status ||= "running"
|
|
62
|
+
self.direction ||= "execute"
|
|
63
|
+
self.epoch ||= 0
|
|
64
|
+
self.resume_count ||= 0
|
|
65
|
+
self.context = {} if context.nil?
|
|
66
|
+
self.last_progress_at ||= Time.now.utc
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
up do
|
|
5
|
+
create_table?(:saga_executions) do
|
|
6
|
+
String :id, size: 36, primary_key: true
|
|
7
|
+
String :saga_name, null: false
|
|
8
|
+
String :resource_type, null: false
|
|
9
|
+
String :resource_id, null: false
|
|
10
|
+
String :status, null: false, default: "running"
|
|
11
|
+
String :current_step
|
|
12
|
+
String :direction, null: false, default: "execute"
|
|
13
|
+
Integer :epoch, null: false, default: 0
|
|
14
|
+
json :context, null: false
|
|
15
|
+
String :correlation_id
|
|
16
|
+
String :failure_reason, text: true
|
|
17
|
+
String :partition
|
|
18
|
+
DateTime :started_at, null: false
|
|
19
|
+
DateTime :finished_at
|
|
20
|
+
DateTime :last_progress_at, null: false
|
|
21
|
+
Integer :resume_count, null: false, default: 0
|
|
22
|
+
DateTime :created_at, null: false
|
|
23
|
+
DateTime :updated_at, null: false
|
|
24
|
+
|
|
25
|
+
index %i[resource_type resource_id created_at], name: :idx_saga_executions_on_resource
|
|
26
|
+
index %i[status updated_at], name: :index_saga_executions_on_status_and_updated_at
|
|
27
|
+
index %i[status last_progress_at], name: :index_saga_executions_on_status_and_progress
|
|
28
|
+
index %i[partition created_at], name: :index_saga_executions_on_partition_and_created_at
|
|
29
|
+
index %i[saga_name created_at], name: :index_saga_executions_on_saga_name_and_created_at
|
|
30
|
+
index %i[status created_at], name: :index_saga_executions_on_status_and_created_at
|
|
31
|
+
index :finished_at, name: :index_saga_executions_on_finished_at
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
create_table?(:saga_step_attempts) do
|
|
35
|
+
String :id, size: 36, primary_key: true
|
|
36
|
+
foreign_key :saga_execution_id, :saga_executions, type: String, size: 36, null: false
|
|
37
|
+
String :step_name, null: false
|
|
38
|
+
String :direction, null: false, default: "execute"
|
|
39
|
+
Integer :epoch, null: false, default: 0
|
|
40
|
+
Integer :attempt, null: false
|
|
41
|
+
String :status, null: false, default: "running"
|
|
42
|
+
String :error_class
|
|
43
|
+
String :error_message, text: true
|
|
44
|
+
String :error_backtrace, text: true
|
|
45
|
+
json :context_after, null: false
|
|
46
|
+
DateTime :started_at, null: false
|
|
47
|
+
DateTime :finished_at
|
|
48
|
+
Float :duration_ms
|
|
49
|
+
DateTime :created_at, null: false
|
|
50
|
+
DateTime :updated_at, null: false
|
|
51
|
+
|
|
52
|
+
index :saga_execution_id, name: :index_saga_step_attempts_on_saga_execution_id
|
|
53
|
+
index %i[saga_execution_id step_name direction epoch attempt], unique: true, name: :idx_saga_step_attempts_dedup
|
|
54
|
+
index :started_at, name: :index_saga_step_attempts_on_started_at
|
|
55
|
+
index %i[error_class started_at], name: :index_saga_step_attempts_on_error_class_and_started_at
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
down do
|
|
60
|
+
drop_table?(:saga_step_attempts, :saga_executions)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The indexes the query methods filter on, for tables created before 001
|
|
4
|
+
# declared them. A no-op on tables created by 001.
|
|
5
|
+
Sequel.migration do
|
|
6
|
+
dashboard_indexes = {
|
|
7
|
+
saga_executions: {
|
|
8
|
+
index_saga_executions_on_saga_name_and_created_at: %i[saga_name created_at],
|
|
9
|
+
index_saga_executions_on_status_and_created_at: %i[status created_at],
|
|
10
|
+
index_saga_executions_on_finished_at: %i[finished_at]
|
|
11
|
+
},
|
|
12
|
+
saga_step_attempts: {
|
|
13
|
+
index_saga_step_attempts_on_started_at: %i[started_at],
|
|
14
|
+
index_saga_step_attempts_on_error_class_and_started_at: %i[error_class started_at]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
up do
|
|
19
|
+
dashboard_indexes.each do |table, named|
|
|
20
|
+
present = indexes(table).keys
|
|
21
|
+
named.each { |name, columns| add_index(table, columns, name: name) unless present.include?(name) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
down do
|
|
26
|
+
dashboard_indexes.each do |table, named|
|
|
27
|
+
present = indexes(table).keys
|
|
28
|
+
named.each { |name, columns| drop_index(table, columns, name: name) if present.include?(name) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Saga
|
|
5
|
+
module Sequel
|
|
6
|
+
# Sequel plugin shared by the saga models: UUIDv7 ids generated in Ruby,
|
|
7
|
+
# created_at/updated_at, validations and a JSON column read back as a
|
|
8
|
+
# Hash whether the driver returns a String or an already parsed value.
|
|
9
|
+
module Record
|
|
10
|
+
JSON_COLUMN = [
|
|
11
|
+
->(value) { JSON.generate(value) },
|
|
12
|
+
->(value) { value.is_a?(String) ? JSON.parse(value) : value.to_h }
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
def self.apply(model, json_column)
|
|
16
|
+
model.plugin :serialization, JSON_COLUMN, json_column
|
|
17
|
+
model.plugin :timestamps, update_on_create: true
|
|
18
|
+
model.plugin :validation_helpers
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.configure(model, _json_column)
|
|
22
|
+
model.raise_on_save_failure = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module InstanceMethods
|
|
26
|
+
def before_create
|
|
27
|
+
self.id ||= SecureRandom.uuid_v7
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Saga
|
|
5
|
+
module Sequel
|
|
6
|
+
class StepAttempt < ::Sequel::Model(Lepus::Saga::Sequel.db[:saga_step_attempts])
|
|
7
|
+
include StepAttemptBehaviour
|
|
8
|
+
|
|
9
|
+
plugin Record, :context_after
|
|
10
|
+
|
|
11
|
+
# The foreign key guards the reference; validating it would load the
|
|
12
|
+
# execution again on every attempt the executor opens under the lock.
|
|
13
|
+
many_to_one :execution, class: "Lepus::Saga::Sequel::Execution", key: :saga_execution_id
|
|
14
|
+
|
|
15
|
+
dataset_module do
|
|
16
|
+
order :chronological, :started_at, :created_at
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate
|
|
20
|
+
super
|
|
21
|
+
validates_presence :step_name
|
|
22
|
+
validates_includes STATUSES, :status
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def before_validation
|
|
26
|
+
self.status ||= "running"
|
|
27
|
+
self.direction ||= "execute"
|
|
28
|
+
self.epoch ||= 0
|
|
29
|
+
self.context_after = {} if context_after.nil?
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "lepus/saga"
|
|
6
|
+
require "sequel"
|
|
7
|
+
require_relative "sequel/version"
|
|
8
|
+
|
|
9
|
+
module Lepus
|
|
10
|
+
module Saga
|
|
11
|
+
# Sequel models for the saga tables. Select the store with
|
|
12
|
+
#
|
|
13
|
+
# Lepus::Saga.configure { |config| config.store = :sequel }
|
|
14
|
+
#
|
|
15
|
+
# The models bind to `Lepus::Saga::Sequel.db` (Sequel::Model.db unless
|
|
16
|
+
# set) the first time they are used, so set it and run the migrations
|
|
17
|
+
# before that.
|
|
18
|
+
module Sequel
|
|
19
|
+
MIGRATIONS_PATH = File.expand_path("sequel/migrations", __dir__)
|
|
20
|
+
MIGRATIONS_TABLE = :lepus_saga_schema_info
|
|
21
|
+
|
|
22
|
+
autoload :Record, "lepus/saga/sequel/record"
|
|
23
|
+
autoload :Execution, "lepus/saga/sequel/execution"
|
|
24
|
+
autoload :StepAttempt, "lepus/saga/sequel/step_attempt"
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
attr_writer :db
|
|
28
|
+
|
|
29
|
+
def db
|
|
30
|
+
@db || ::Sequel::Model.db
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Creates (or rolls back, with `target:`) the saga tables, tracking the
|
|
34
|
+
# version in its own table so it never collides with the app's
|
|
35
|
+
# migrations.
|
|
36
|
+
def migrate(db = self.db, **options)
|
|
37
|
+
::Sequel.extension :migration
|
|
38
|
+
::Sequel::Migrator.run(db, MIGRATIONS_PATH, table: MIGRATIONS_TABLE, **options)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
require_relative "stores/sequel"
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lepus/saga/sequel"
|
|
4
|
+
|
|
5
|
+
module Lepus
|
|
6
|
+
module Saga
|
|
7
|
+
module Stores
|
|
8
|
+
# Executions and step attempts as Sequel models. Locks are
|
|
9
|
+
# SELECT ... FOR UPDATE inside a transaction; on SQLite, where FOR UPDATE
|
|
10
|
+
# does not exist, transactions open as IMMEDIATE, which serializes
|
|
11
|
+
# writers on the whole database instead. Pass other model classes to use
|
|
12
|
+
# subclasses with their own datasets.
|
|
13
|
+
#
|
|
14
|
+
# The query methods use portable SQL only (filters, GROUP BY, COUNT,
|
|
15
|
+
# MAX), so they behave the same on SQLite, PostgreSQL and MySQL/MariaDB:
|
|
16
|
+
# time buckets and percentiles are computed in Ruby over at most
|
|
17
|
+
# `max_rows` selected values.
|
|
18
|
+
class Sequel < Base
|
|
19
|
+
def initialize(execution_class: nil, step_attempt_class: nil)
|
|
20
|
+
super()
|
|
21
|
+
@execution_class = execution_class
|
|
22
|
+
@step_attempt_class = step_attempt_class
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execution_class
|
|
26
|
+
@execution_class ||= Lepus::Saga::Sequel::Execution
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def step_attempt_class
|
|
30
|
+
@step_attempt_class ||= Lepus::Saga::Sequel::StepAttempt
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_execution(attributes)
|
|
34
|
+
execution_class.create(attributes)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_execution(id)
|
|
38
|
+
execution_class[id]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def save_execution(execution)
|
|
42
|
+
execution.save
|
|
43
|
+
execution
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_lock(execution)
|
|
47
|
+
transaction do
|
|
48
|
+
execution.lock!
|
|
49
|
+
yield execution
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def with_locked_execution(id)
|
|
54
|
+
transaction { yield execution_class.for_update.first(id: id) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def increment_resume_count(execution)
|
|
58
|
+
dataset = execution_class.where(id: execution.id)
|
|
59
|
+
dataset.update(resume_count: ::Sequel[:resume_count] + 1, updated_at: Time.now.utc)
|
|
60
|
+
execution.values[:resume_count] = dataset.get(:resume_count)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def stalled_executions(before:)
|
|
64
|
+
execution_class.in_progress.where(::Sequel[:last_progress_at] < before).order(:last_progress_at).all
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def executions_for_resource(resource_type, resource_id)
|
|
68
|
+
execution_class.for_resource(resource_type, resource_id).recent_first.all
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_step_attempt(execution, attributes)
|
|
72
|
+
step_attempt_class.create(attributes.merge(saga_execution_id: execution.id))
|
|
73
|
+
rescue ::Sequel::UniqueConstraintViolation => error
|
|
74
|
+
raise DuplicateAttemptError, error.message
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def save_step_attempt(attempt)
|
|
78
|
+
attempt.save
|
|
79
|
+
attempt
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def step_attempts(execution)
|
|
83
|
+
attempts_of(execution).chronological.all
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def count_step_attempts(execution, step_name:, direction:, epoch:)
|
|
87
|
+
attempts_of(execution).where(step_name: step_name, direction: direction, epoch: epoch).count
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def running_step_attempt?(execution, step_name:, direction:, epoch:, started_since:)
|
|
91
|
+
!attempts_of(execution)
|
|
92
|
+
.where(step_name: step_name, direction: direction, epoch: epoch, status: "running")
|
|
93
|
+
.where(::Sequel[:started_at] >= started_since)
|
|
94
|
+
.empty?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def stale_step_attempts(execution, step_name:, direction:, epoch:, started_before:)
|
|
98
|
+
attempts_of(execution)
|
|
99
|
+
.where(step_name: step_name, direction: direction, epoch: epoch, status: "running")
|
|
100
|
+
.where(::Sequel[:started_at] < started_before)
|
|
101
|
+
.all
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def step_succeeded?(execution, step_name)
|
|
105
|
+
!attempts_of(execution).where(step_name: step_name, direction: "execute", status: "succeeded").empty?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def queries? = true
|
|
109
|
+
|
|
110
|
+
def list_executions(before_id: nil, limit: 50, **filters)
|
|
111
|
+
scope = matching_executions(**filters)
|
|
112
|
+
scope = scope.where(::Sequel[:id] < before_id) if before_id
|
|
113
|
+
scope.order(::Sequel.desc(:id)).limit(limit).all
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def count_executions(group_by:, **filters)
|
|
117
|
+
keys = group_keys(group_by)
|
|
118
|
+
columns = keys.include?(:partition) ? keys | [:saga_name] : keys
|
|
119
|
+
rows = matching_executions(**filters).group_and_count(*columns).naked.all
|
|
120
|
+
tally_groups(rows.map { |row| [row.slice(*columns), row[:count]] }, group_by)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def execution_series(from:, to:, bucket:, saga_name: nil, max_rows: QUERY_MAX_ROWS)
|
|
124
|
+
seconds = series_bucket(from, to, bucket)
|
|
125
|
+
scope = matching_executions(saga_name: saga_name)
|
|
126
|
+
started, started_truncated = take_rows(
|
|
127
|
+
in_window(scope, :created_at, from, to)
|
|
128
|
+
.order(::Sequel.desc(:created_at)).limit(max_rows + 1).select_map(:created_at),
|
|
129
|
+
max_rows
|
|
130
|
+
)
|
|
131
|
+
finished, finished_truncated = take_rows(
|
|
132
|
+
in_window(scope.where(status: ExecutionBehaviour::TERMINAL_STATUSES), :finished_at, from, to)
|
|
133
|
+
.order(::Sequel.desc(:finished_at)).limit(max_rows + 1).select_map(%i[finished_at status]),
|
|
134
|
+
max_rows
|
|
135
|
+
)
|
|
136
|
+
{rows: build_series(from, to, seconds, started, finished), truncated: started_truncated || finished_truncated}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def step_latency(from:, to:, saga_name: nil, max_rows: QUERY_MAX_ROWS)
|
|
140
|
+
samples, truncated = take_rows(
|
|
141
|
+
attempts_with_saga(from, to, saga_name)
|
|
142
|
+
.where(attempts_table[:status] => LATENCY_STATUSES)
|
|
143
|
+
.exclude(attempts_table[:duration_ms] => nil)
|
|
144
|
+
.order(::Sequel.desc(attempts_table[:started_at])).limit(max_rows + 1)
|
|
145
|
+
.select_map([executions_table[:saga_name], attempts_table[:step_name], attempts_table[:direction], attempts_table[:duration_ms]]),
|
|
146
|
+
max_rows
|
|
147
|
+
)
|
|
148
|
+
{rows: latency_rows(samples), truncated: truncated}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def failure_summary(from:, to:, saga_name: nil, limit: 20)
|
|
152
|
+
attempts = attempts_table
|
|
153
|
+
saga = executions_table[:saga_name]
|
|
154
|
+
count = ::Sequel.function(:count).*
|
|
155
|
+
failed = attempts_with_saga(from, to, saga_name).where(attempts[:status] => "failed")
|
|
156
|
+
failed
|
|
157
|
+
.group(attempts[:error_class], saga, attempts[:step_name])
|
|
158
|
+
.order(::Sequel.desc(count), ::Sequel.desc(::Sequel.function(:max, attempts[:finished_at])))
|
|
159
|
+
.limit(limit)
|
|
160
|
+
.select_map([
|
|
161
|
+
attempts[:error_class], saga, attempts[:step_name], count.as(:count),
|
|
162
|
+
::Sequel.function(:count, attempts[:saga_execution_id]).distinct.as(:execution_count)
|
|
163
|
+
])
|
|
164
|
+
.map do |error_class, group_saga, step_name, group_count, execution_count|
|
|
165
|
+
message, last_seen_at, execution_id = failed
|
|
166
|
+
.where(attempts[:error_class] => error_class, attempts[:step_name] => step_name, saga => group_saga)
|
|
167
|
+
.order(::Sequel.desc(attempts[:finished_at]), ::Sequel.desc(attempts[:id]))
|
|
168
|
+
.get([attempts[:error_message], attempts[:finished_at], attempts[:saga_execution_id]])
|
|
169
|
+
{
|
|
170
|
+
error_class: error_class, saga_name: group_saga, step_name: step_name, count: group_count.to_i,
|
|
171
|
+
execution_count: execution_count.to_i, last_seen_at: last_seen_at, sample_message: message, sample_execution_id: execution_id
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def saga_names
|
|
177
|
+
execution_class.distinct.order(:saga_name).select_map(:saga_name)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def db = execution_class.db
|
|
183
|
+
|
|
184
|
+
# SQLite has no FOR UPDATE: an IMMEDIATE transaction takes the write
|
|
185
|
+
# lock up front, so a second locker waits instead of reading alongside.
|
|
186
|
+
# Sequel's SQLite adapter wraps an ArgumentError raised in the block in
|
|
187
|
+
# Sequel::DatabaseError; the caller gets its own error back.
|
|
188
|
+
def transaction(&)
|
|
189
|
+
return db.transaction(&) unless db.database_type == :sqlite
|
|
190
|
+
|
|
191
|
+
begin
|
|
192
|
+
db.transaction(mode: :immediate, &)
|
|
193
|
+
rescue ::Sequel::DatabaseError => error
|
|
194
|
+
raise error unless error.wrapped_exception.is_a?(ArgumentError)
|
|
195
|
+
|
|
196
|
+
raise error.wrapped_exception, cause: nil
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def attempts_of(execution)
|
|
201
|
+
step_attempt_class.where(saga_execution_id: execution.id)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def executions_table = ::Sequel[execution_class.table_name]
|
|
205
|
+
|
|
206
|
+
def attempts_table = ::Sequel[step_attempt_class.table_name]
|
|
207
|
+
|
|
208
|
+
def in_window(scope, column, from, to)
|
|
209
|
+
scope = scope.where(::Sequel[column] >= from) if from
|
|
210
|
+
scope = scope.where(::Sequel[column] < to) if to
|
|
211
|
+
scope
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def matching_executions(saga_name: nil, status: nil, current_step: nil, partition: nil, resource: nil, from: nil, to: nil,
|
|
215
|
+
progress_from: nil, progress_to: nil, error_class: nil, error_step: nil, error_from: nil, error_to: nil)
|
|
216
|
+
sagas = filter_values(saga_name)
|
|
217
|
+
statuses = filter_values(status)
|
|
218
|
+
steps = filter_values(current_step)
|
|
219
|
+
scope = execution_class.dataset
|
|
220
|
+
scope = scope.where(saga_name: sagas) if sagas
|
|
221
|
+
scope = scope.where(status: statuses) if statuses
|
|
222
|
+
scope = scope.where(current_step: steps) if steps
|
|
223
|
+
scope = in_window(scope, :created_at, from, to)
|
|
224
|
+
scope = in_window(scope, :last_progress_at, progress_from, progress_to)
|
|
225
|
+
failing = failed_attempts(filter_values(error_class), filter_values(error_step), error_from, error_to)
|
|
226
|
+
scope = scope.where(id: failing.select(:saga_execution_id)) if failing
|
|
227
|
+
scope = scope.where(in_partition(partition.to_s)) unless blank?(partition)
|
|
228
|
+
scope = scope.where(matching_resource(scope, resource.to_s)) unless blank?(resource)
|
|
229
|
+
scope
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def failed_attempts(classes, steps, from, to)
|
|
233
|
+
return if [classes, steps, from, to].all?(&:nil?)
|
|
234
|
+
|
|
235
|
+
scope = step_attempt_class.where(status: "failed")
|
|
236
|
+
scope = scope.where(error_class: classes) if classes
|
|
237
|
+
scope = scope.where(step_name: steps) if steps
|
|
238
|
+
in_window(scope, :started_at, from, to)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def in_partition(partition)
|
|
242
|
+
::Sequel.|({partition: partition}, {partition: nil, saga_name: partition_saga_names(partition)})
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def matching_resource(scope, needle)
|
|
246
|
+
pattern = "%#{scope.escape_like(needle)}%"
|
|
247
|
+
::Sequel.|(::Sequel.ilike(:resource_type, pattern), ::Sequel.ilike(:resource_id, pattern))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Attempts started in the window, joined to their execution. Columns
|
|
251
|
+
# are qualified from here on: both tables have id, status and the
|
|
252
|
+
# timestamps.
|
|
253
|
+
def attempts_with_saga(from, to, saga_name)
|
|
254
|
+
scope = step_attempt_class.dataset
|
|
255
|
+
.join(execution_class.table_name, id: :saga_execution_id)
|
|
256
|
+
.where(attempts_table[:started_at] >= from)
|
|
257
|
+
.where(attempts_table[:started_at] < to)
|
|
258
|
+
sagas = filter_values(saga_name)
|
|
259
|
+
sagas ? scope.where(executions_table[:saga_name] => sagas) : scope
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def blank?(value) = value.to_s.strip.empty?
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
data/mise.toml
ADDED
metadata
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lepus-saga-sequel
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Marcos G. Zimmermann
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: lepus-saga
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.3.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.3.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sequel
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.60'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.60'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: pry
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rubocop
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rubocop-performance
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rubocop-rspec
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: standard
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
description: Keeps lepus-saga executions and step attempts in Sequel models (PostgreSQL,
|
|
139
|
+
MySQL/MariaDB, SQLite), with migrations for the tables
|
|
140
|
+
email:
|
|
141
|
+
- mgzmaster@gmail.com
|
|
142
|
+
executables: []
|
|
143
|
+
extensions: []
|
|
144
|
+
extra_rdoc_files: []
|
|
145
|
+
files:
|
|
146
|
+
- CHANGELOG.md
|
|
147
|
+
- Gemfile
|
|
148
|
+
- LICENSE.txt
|
|
149
|
+
- README.md
|
|
150
|
+
- Rakefile
|
|
151
|
+
- docs/brand/BRAND.md
|
|
152
|
+
- docs/brand/apple-touch-icon.png
|
|
153
|
+
- docs/brand/favicon-16.png
|
|
154
|
+
- docs/brand/favicon-32.png
|
|
155
|
+
- docs/brand/favicon.ico
|
|
156
|
+
- docs/brand/favicon.svg
|
|
157
|
+
- docs/brand/icon-512.png
|
|
158
|
+
- docs/brand/lockup-lepus-mono.svg
|
|
159
|
+
- docs/brand/lockup-lepus-on-dark.svg
|
|
160
|
+
- docs/brand/lockup-lepus-on-light.svg
|
|
161
|
+
- docs/brand/lockup-lepus-saga-mono.svg
|
|
162
|
+
- docs/brand/lockup-lepus-saga-on-dark.svg
|
|
163
|
+
- docs/brand/lockup-lepus-saga-on-light.svg
|
|
164
|
+
- docs/brand/lockup-lepus-saga.svg
|
|
165
|
+
- docs/brand/lockup-lepus.svg
|
|
166
|
+
- docs/brand/mark-mono.svg
|
|
167
|
+
- docs/brand/mark-on-dark.svg
|
|
168
|
+
- docs/brand/mark-on-light.svg
|
|
169
|
+
- docs/brand/mark-small-mono.svg
|
|
170
|
+
- docs/brand/mark-small-on-dark.svg
|
|
171
|
+
- docs/brand/mark-small-on-light.svg
|
|
172
|
+
- docs/brand/mark-small.svg
|
|
173
|
+
- docs/brand/mark.svg
|
|
174
|
+
- lib/lepus/saga/sequel.rb
|
|
175
|
+
- lib/lepus/saga/sequel/execution.rb
|
|
176
|
+
- lib/lepus/saga/sequel/migrations/001_create_lepus_saga_tables.rb
|
|
177
|
+
- lib/lepus/saga/sequel/migrations/002_add_lepus_saga_dashboard_indexes.rb
|
|
178
|
+
- lib/lepus/saga/sequel/record.rb
|
|
179
|
+
- lib/lepus/saga/sequel/step_attempt.rb
|
|
180
|
+
- lib/lepus/saga/sequel/version.rb
|
|
181
|
+
- lib/lepus/saga/stores/sequel.rb
|
|
182
|
+
- mise.toml
|
|
183
|
+
homepage: https://github.com/marcosgz/lepus-saga-sequel
|
|
184
|
+
licenses:
|
|
185
|
+
- MIT
|
|
186
|
+
metadata:
|
|
187
|
+
homepage_uri: https://github.com/marcosgz/lepus-saga-sequel
|
|
188
|
+
source_code_uri: https://github.com/marcosgz/lepus-saga-sequel
|
|
189
|
+
changelog_uri: https://github.com/marcosgz/lepus-saga-sequel/blob/master/CHANGELOG.md
|
|
190
|
+
rdoc_options: []
|
|
191
|
+
require_paths:
|
|
192
|
+
- lib
|
|
193
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
194
|
+
requirements:
|
|
195
|
+
- - ">="
|
|
196
|
+
- !ruby/object:Gem::Version
|
|
197
|
+
version: 3.3.0
|
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
199
|
+
requirements:
|
|
200
|
+
- - ">="
|
|
201
|
+
- !ruby/object:Gem::Version
|
|
202
|
+
version: '0'
|
|
203
|
+
requirements: []
|
|
204
|
+
rubygems_version: 3.6.9
|
|
205
|
+
specification_version: 4
|
|
206
|
+
summary: Sequel store for Lepus::Saga
|
|
207
|
+
test_files: []
|