lepus-saga-active_record 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 +19 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +160 -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/generators/lepus/saga/dashboard_indexes/dashboard_indexes_generator.rb +26 -0
- data/lib/generators/lepus/saga/dashboard_indexes/templates/add_lepus_saga_dashboard_indexes.rb.erb +17 -0
- data/lib/generators/lepus/saga/install/install_generator.rb +26 -0
- data/lib/generators/lepus/saga/install/templates/create_lepus_saga_tables.rb.erb +52 -0
- data/lib/lepus/saga/active_record/execution.rb +60 -0
- data/lib/lepus/saga/active_record/record.rb +13 -0
- data/lib/lepus/saga/active_record/step_attempt.rb +30 -0
- data/lib/lepus/saga/active_record/version.rb +9 -0
- data/lib/lepus/saga/active_record.rb +20 -0
- data/lib/lepus/saga/stores/active_record.rb +235 -0
- data/mise.toml +2 -0
- metadata +237 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 72048dd94435e22305e296501e08f2d19234da3081fbf3c4c2fd75afdecd79f9
|
|
4
|
+
data.tar.gz: df6d77f4f98b6596449de33c4fba15a479d74e5c285f8cc406776727e7d72328
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '09d4fbe4123daf7310d7a387f63db9f0e9e4b4055eff5035a3bb8c7285a547d53fecc7850dab249d780fa72aa7d19fb3d8e61382b77e1f01a00c5f9b6bfc404d'
|
|
7
|
+
data.tar.gz: 9ace56afa8214880ae7d97b39347e6cefdd5906095befb3bbd01064b5e856fcf41555f985b8792ab7f65322ea1f4d4f218d9df637281e7e7c15950676838fc34
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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::ActiveRecord`, selected with `Lepus::Saga.configure { |c| c.store = :active_record }`
|
|
10
|
+
* Added: `Lepus::Saga::ActiveRecord::Execution` and `StepAttempt` models (UUIDv7 ids, json context, read scopes) on the `saga_executions` and `saga_step_attempts` tables
|
|
11
|
+
* Added: `rails g lepus:saga:install` (moved from lepus-saga): `failure_reason` is `text` and there is no `lock_version` column
|
|
12
|
+
* Changed: optimistic locking is off, so a legacy `lock_version` column (tables created before the generator) is ignored; `resume_count` is incremented atomically
|
|
13
|
+
* Added: `Execution.prune(finished_before:)` deletes completed executions and their step attempts in batches
|
|
14
|
+
* Changed: the resource index is `(resource_type, resource_id, created_at)`, which serves `executions_for_resource`, and `activerecord >= 8.0` is required
|
|
15
|
+
* 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
|
|
16
|
+
* Added: `lepus:saga:install` indexes `saga_executions (saga_name, created_at)`, `(status, created_at)`, `(finished_at)` and `saga_step_attempts (started_at)`, `(error_class, started_at)`
|
|
17
|
+
* Added: `rails g lepus:saga:dashboard_indexes` adds those indexes to existing tables
|
|
18
|
+
* Added: the `current_step` filter of `list_executions` and `count_executions`
|
|
19
|
+
* Added: the `progress_from`/`progress_to` and `error_class`/`error_step`/`error_from`/`error_to` filters of `list_executions` and `count_executions`, and `execution_count` on `failure_summary` rows
|
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,160 @@
|
|
|
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-active_record</code></p>
|
|
9
|
+
|
|
10
|
+
ActiveRecord 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-active_record"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
bin/rails generate lepus:saga:install
|
|
22
|
+
bin/rails db:migrate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The generator writes one migration that creates both tables. 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).
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# config/initializers/lepus_saga.rb
|
|
29
|
+
Lepus::Saga.configure do |config|
|
|
30
|
+
config.store = :active_record
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`:active_record` requires `lepus/saga/stores/active_record` and instantiates `Lepus::Saga::Stores::ActiveRecord`. To use your own subclasses of the models (another table name, `connects_to` another database), pass an instance:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
config.store = Lepus::Saga::Stores::ActiveRecord.new(execution_class: Saga::Execution, step_attempt_class: Saga::StepAttempt)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The rest of the setup (definitions, consumers, operations, instrumentation) is in the [lepus-saga README](https://github.com/marcosgz/lepus-saga#readme).
|
|
41
|
+
|
|
42
|
+
## Models
|
|
43
|
+
|
|
44
|
+
`Lepus::Saga::ActiveRecord::Execution` and `Lepus::Saga::ActiveRecord::StepAttempt` inherit from the abstract `Lepus::Saga::ActiveRecord::Record` and include the core's `ExecutionBehaviour` and `StepAttemptBehaviour`, so they answer `running?`, `terminal?`, `definition_class`, `partition` and the rest.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
Lepus::Saga::ActiveRecord::Execution.for_resource("Order", order.id).completed.exists?
|
|
48
|
+
Lepus::Saga::ActiveRecord::Execution.in_progress.where(partition: "organic")
|
|
49
|
+
Lepus::Saga::ActiveRecord::Execution.recent_first.limit(10)
|
|
50
|
+
Lepus::Saga::ActiveRecord::Execution.stalled(15.minutes)
|
|
51
|
+
execution.step_attempts.chronological
|
|
52
|
+
execution.attempts_for_current_step
|
|
53
|
+
Lepus::Saga::ActiveRecord::Execution::STATUSES
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Nothing deletes old rows on its own. Prune completed executions (and their step attempts) from a recurring job; call it on a relation to keep some:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Lepus::Saga::ActiveRecord::Execution.prune(finished_before: 30.days.ago)
|
|
60
|
+
Lepus::Saga::ActiveRecord::Execution.where.not(id: pinned_ids).prune(finished_before: 30.days.ago)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How it maps the store contract
|
|
64
|
+
|
|
65
|
+
| Store operation | ActiveRecord |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `with_lock(execution)` | `execution.with_lock` (reload with `SELECT ... FOR UPDATE` in a transaction) |
|
|
68
|
+
| `with_locked_execution(id)` | `transaction { Execution.lock.find_by(id:) }`, one query |
|
|
69
|
+
| `increment_resume_count` | `update_counters(id, resume_count: 1, touch: true)`, then reads the stored value back |
|
|
70
|
+
| `create_step_attempt` | `create!`; `ActiveRecord::RecordNotUnique` from `idx_saga_step_attempts_dedup` becomes `Lepus::Saga::DuplicateAttemptError` |
|
|
71
|
+
| `stalled_executions(before:)` | `in_progress.where(last_progress_at: ...before).order(:last_progress_at)` |
|
|
72
|
+
| attempt queries | `where(saga_execution_id:, step_name:, direction:, epoch:)` plus `count`/`exists?` |
|
|
73
|
+
| `list_executions` | Filters on the executions table, `order(id: :desc)` with `id < before_id` for keyset paging; the resource search is `LOWER(column) LIKE ? ESCAPE '!'`; the error filters are `id IN (SELECT saga_execution_id FROM saga_step_attempts WHERE status = 'failed' ...)` |
|
|
74
|
+
| `count_executions` | `group(...).count`; grouping by partition also groups by saga name to resolve executions without one |
|
|
75
|
+
| `execution_series` | Plucks `created_at`, and `finished_at` and `status` of terminal executions, newest first and at most `max_rows` + 1; buckets them in Ruby |
|
|
76
|
+
| `step_latency` | Joins attempts to executions and plucks the newest `max_rows` + 1 durations; percentiles in Ruby |
|
|
77
|
+
| `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 |
|
|
78
|
+
| `saga_names` | `distinct.order(:saga_name).pluck(:saga_name)` |
|
|
79
|
+
|
|
80
|
+
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`.
|
|
81
|
+
|
|
82
|
+
On SQLite `FOR UPDATE` is a no-op; Rails 8 opens SQLite transactions as `IMMEDIATE`, which serializes writers on the whole database instead of one row.
|
|
83
|
+
|
|
84
|
+
### No optimistic locking
|
|
85
|
+
|
|
86
|
+
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 and would turn a legacy `lock_version` column into `ActiveRecord::StaleObjectError` on any write from a stale copy. So `Execution.lock_optimistically = false`, and the generator no longer creates `lock_version`. Tables that already have it keep working: the column has a default and is ignored.
|
|
87
|
+
|
|
88
|
+
The `belongs_to :execution` of a step attempt is optional, because the foreign key already guards it and validating it would load the execution again on every attempt the executor opens under the lock.
|
|
89
|
+
|
|
90
|
+
## Existing tables
|
|
91
|
+
|
|
92
|
+
Tables created by hand or before the generator (PostgreSQL `uuid` ids with `gen_random_uuid()` defaults, `jsonb` context, a `lock_version` column) work unchanged: the models set UUIDv7 ids before insert, read `jsonb` through the `json` attribute type and ignore `lock_version`.
|
|
93
|
+
|
|
94
|
+
Tables created before the install migration indexed the query columns lack the indexes the query methods filter on. Add them with:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bin/rails generate lepus:saga:dashboard_indexes
|
|
98
|
+
bin/rails db:migrate
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The 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.
|
|
102
|
+
|
|
103
|
+
## Architecture
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
lepus-saga lepus-saga-active_record
|
|
107
|
+
────────── ────────────────────────
|
|
108
|
+
Lepus::Saga::Stores::Base <─── Lepus::Saga::Stores::ActiveRecord
|
|
109
|
+
ExecutionBehaviour <─── Lepus::Saga::ActiveRecord::Execution
|
|
110
|
+
StepAttemptBehaviour <─── Lepus::Saga::ActiveRecord::StepAttempt
|
|
111
|
+
"a lepus saga store" ───> spec/lepus/saga/stores/active_record_spec.rb
|
|
112
|
+
"a lepus saga engine" ───> (contract and full engine on every database)
|
|
113
|
+
"a lepus saga query store" ───>
|
|
114
|
+
lib/generators/lepus/saga/install
|
|
115
|
+
lib/generators/lepus/saga/dashboard_indexes
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This gem is also the reference for writing another adapter (Sequel, ROM, a key-value store):
|
|
119
|
+
|
|
120
|
+
1. Name the gem `lepus-saga-<name>` and define `Lepus::Saga::Stores::<Name> < Lepus::Saga::Stores::Base` in `lib/lepus/saga/stores/<name>.rb`, so `config.store = :<name>` finds it.
|
|
121
|
+
2. Include `Lepus::Saga::ExecutionBehaviour` and `Lepus::Saga::StepAttemptBehaviour` into your record classes.
|
|
122
|
+
3. Implement each method of `Stores::Base` documented in lepus-saga's `lib/lepus/saga/stores/base.rb`.
|
|
123
|
+
4. Run the shared examples against every database you support:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
require "lepus/saga/rspec"
|
|
127
|
+
|
|
128
|
+
RSpec.describe Lepus::Saga::Stores::Sequel do
|
|
129
|
+
let(:store) { described_class.new }
|
|
130
|
+
|
|
131
|
+
it_behaves_like "a lepus saga store"
|
|
132
|
+
it_behaves_like "a lepus saga engine"
|
|
133
|
+
it_behaves_like "a lepus saga query store" # only if the store implements the optional query methods
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The lepus-saga README has a Sequel sketch.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
bin/setup
|
|
143
|
+
bundle exec rspec # SQLite file in tmp/
|
|
144
|
+
bundle exec rubocop
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Against PostgreSQL and MariaDB, start the databases with `docker compose up -d` and run:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5433/lepus_saga_test BUNDLE_GEMFILE=ci/Gemfile.postgresql bundle exec rspec
|
|
151
|
+
DATABASE_URL=mysql2://root:root@127.0.0.1:3317/lepus_saga_test BUNDLE_GEMFILE=ci/Gemfile.mariadb bundle exec rspec
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Examples tagged `:row_locks` (one row locked while another stays free, a single `FOR UPDATE` query) are skipped on SQLite.
|
|
155
|
+
|
|
156
|
+
The Gemfile uses `../lepus-saga` when it exists (or `LEPUS_SAGA_PATH`), and the GitHub repository otherwise.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
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,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Lepus
|
|
7
|
+
module Saga
|
|
8
|
+
class DashboardIndexesGenerator < Rails::Generators::Base
|
|
9
|
+
include ::ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Adds the indexes the store's query methods use to saga tables created before they were in lepus:saga:install"
|
|
14
|
+
|
|
15
|
+
def create_migration_file
|
|
16
|
+
migration_template "add_lepus_saga_dashboard_indexes.rb.erb", File.join(db_migrate_path, "add_lepus_saga_dashboard_indexes.rb")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def migration_version
|
|
22
|
+
"[#{::ActiveRecord::VERSION::MAJOR}.#{::ActiveRecord::VERSION::MINOR}]"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/generators/lepus/saga/dashboard_indexes/templates/add_lepus_saga_dashboard_indexes.rb.erb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class AddLepusSagaDashboardIndexes < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
add_index :saga_executions, [:saga_name, :created_at], if_not_exists: true
|
|
4
|
+
add_index :saga_executions, [:status, :created_at], if_not_exists: true
|
|
5
|
+
add_index :saga_executions, :finished_at, if_not_exists: true
|
|
6
|
+
add_index :saga_step_attempts, :started_at, if_not_exists: true
|
|
7
|
+
add_index :saga_step_attempts, [:error_class, :started_at], if_not_exists: true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def down
|
|
11
|
+
remove_index :saga_step_attempts, [:error_class, :started_at], if_exists: true
|
|
12
|
+
remove_index :saga_step_attempts, :started_at, if_exists: true
|
|
13
|
+
remove_index :saga_executions, :finished_at, if_exists: true
|
|
14
|
+
remove_index :saga_executions, [:status, :created_at], if_exists: true
|
|
15
|
+
remove_index :saga_executions, [:saga_name, :created_at], if_exists: true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Lepus
|
|
7
|
+
module Saga
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include ::ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Creates the migration for the saga_executions and saga_step_attempts tables"
|
|
14
|
+
|
|
15
|
+
def create_migration_file
|
|
16
|
+
migration_template "create_lepus_saga_tables.rb.erb", File.join(db_migrate_path, "create_lepus_saga_tables.rb")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def migration_version
|
|
22
|
+
"[#{::ActiveRecord::VERSION::MAJOR}.#{::ActiveRecord::VERSION::MINOR}]"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class CreateLepusSagaTables < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :saga_executions, id: :string, limit: 36 do |t|
|
|
4
|
+
t.string :saga_name, null: false
|
|
5
|
+
t.string :resource_type, null: false
|
|
6
|
+
t.string :resource_id, null: false
|
|
7
|
+
t.string :status, null: false, default: "running"
|
|
8
|
+
t.string :current_step
|
|
9
|
+
t.string :direction, null: false, default: "execute"
|
|
10
|
+
t.integer :epoch, null: false, default: 0
|
|
11
|
+
t.json :context, null: false
|
|
12
|
+
t.string :correlation_id
|
|
13
|
+
t.text :failure_reason
|
|
14
|
+
t.string :partition
|
|
15
|
+
t.datetime :started_at, null: false
|
|
16
|
+
t.datetime :finished_at
|
|
17
|
+
t.datetime :last_progress_at, null: false
|
|
18
|
+
t.integer :resume_count, null: false, default: 0
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
add_index :saga_executions, [:resource_type, :resource_id, :created_at],
|
|
22
|
+
name: "idx_saga_executions_on_resource"
|
|
23
|
+
add_index :saga_executions, [:status, :updated_at]
|
|
24
|
+
add_index :saga_executions, [:status, :last_progress_at],
|
|
25
|
+
name: "index_saga_executions_on_status_and_progress"
|
|
26
|
+
add_index :saga_executions, [:partition, :created_at]
|
|
27
|
+
add_index :saga_executions, [:saga_name, :created_at]
|
|
28
|
+
add_index :saga_executions, [:status, :created_at]
|
|
29
|
+
add_index :saga_executions, :finished_at
|
|
30
|
+
|
|
31
|
+
create_table :saga_step_attempts, id: :string, limit: 36 do |t|
|
|
32
|
+
t.references :saga_execution, type: :string, limit: 36, null: false, foreign_key: true
|
|
33
|
+
t.string :step_name, null: false
|
|
34
|
+
t.string :direction, null: false, default: "execute"
|
|
35
|
+
t.integer :epoch, null: false, default: 0
|
|
36
|
+
t.integer :attempt, null: false
|
|
37
|
+
t.string :status, null: false, default: "running"
|
|
38
|
+
t.string :error_class
|
|
39
|
+
t.text :error_message
|
|
40
|
+
t.text :error_backtrace
|
|
41
|
+
t.json :context_after, null: false
|
|
42
|
+
t.datetime :started_at, null: false
|
|
43
|
+
t.datetime :finished_at
|
|
44
|
+
t.float :duration_ms
|
|
45
|
+
t.timestamps
|
|
46
|
+
end
|
|
47
|
+
add_index :saga_step_attempts, [:saga_execution_id, :step_name, :direction, :epoch, :attempt],
|
|
48
|
+
unique: true, name: "idx_saga_step_attempts_dedup"
|
|
49
|
+
add_index :saga_step_attempts, :started_at
|
|
50
|
+
add_index :saga_step_attempts, [:error_class, :started_at]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Saga
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
class Execution < Record
|
|
7
|
+
include ExecutionBehaviour
|
|
8
|
+
|
|
9
|
+
self.table_name = "saga_executions"
|
|
10
|
+
# Concurrency is the row lock plus the epoch fence. A legacy
|
|
11
|
+
# lock_version column would otherwise turn every write made outside
|
|
12
|
+
# the lock into a possible StaleObjectError.
|
|
13
|
+
self.lock_optimistically = false
|
|
14
|
+
|
|
15
|
+
attribute :context, :json, default: -> { {} }
|
|
16
|
+
attribute :last_progress_at, :datetime, default: -> { Time.current }
|
|
17
|
+
|
|
18
|
+
has_many :step_attempts,
|
|
19
|
+
class_name: "Lepus::Saga::ActiveRecord::StepAttempt",
|
|
20
|
+
foreign_key: :saga_execution_id,
|
|
21
|
+
inverse_of: :execution,
|
|
22
|
+
dependent: :delete_all
|
|
23
|
+
|
|
24
|
+
before_create { self.id ||= SecureRandom.uuid_v7 }
|
|
25
|
+
|
|
26
|
+
validates :saga_name, :resource_type, :resource_id, presence: true
|
|
27
|
+
validates :status, inclusion: {in: STATUSES}
|
|
28
|
+
validates :direction, inclusion: {in: DIRECTIONS}
|
|
29
|
+
|
|
30
|
+
scope :for_resource, ->(type, id) { where(resource_type: type, resource_id: id) }
|
|
31
|
+
scope :in_progress, -> { where(status: IN_PROGRESS_STATUSES) }
|
|
32
|
+
scope :completed, -> { where(status: "completed") }
|
|
33
|
+
scope :recent_first, -> { order(created_at: :desc) }
|
|
34
|
+
# Deliberately not `updated_at`: a re-publish, a lock or an attempt
|
|
35
|
+
# opened and abandoned all touch the row while the cursor stands
|
|
36
|
+
# still, so a wedged execution would keep resetting its own stall clock.
|
|
37
|
+
scope :stalled, ->(older_than = 15.minutes) { in_progress.where(last_progress_at: ...older_than.ago) }
|
|
38
|
+
|
|
39
|
+
# Deletes completed executions that finished before the cutoff, with
|
|
40
|
+
# their step attempts, batch by batch. Call it on a relation to keep
|
|
41
|
+
# some: `where.not(id: ids_to_keep).prune(finished_before: 30.days.ago)`.
|
|
42
|
+
# Returns the number of executions deleted.
|
|
43
|
+
def self.prune(finished_before:, batch_size: 1_000)
|
|
44
|
+
step_attempt_class = reflect_on_association(:step_attempts).klass
|
|
45
|
+
completed.where(finished_at: ...finished_before).in_batches(of: batch_size).sum do |batch|
|
|
46
|
+
ids = batch.pluck(:id)
|
|
47
|
+
transaction do
|
|
48
|
+
step_attempt_class.where(saga_execution_id: ids).delete_all
|
|
49
|
+
unscoped.where(id: ids).delete_all
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def attempts_for_current_step
|
|
55
|
+
step_attempts.where(current_step_scope)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Saga
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
# Abstract parent of the saga models, so an app can point them at another
|
|
7
|
+
# database with `connects_to` without touching its own models.
|
|
8
|
+
class Record < ::ActiveRecord::Base
|
|
9
|
+
self.abstract_class = true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
module Saga
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
class StepAttempt < Record
|
|
7
|
+
include StepAttemptBehaviour
|
|
8
|
+
|
|
9
|
+
self.table_name = "saga_step_attempts"
|
|
10
|
+
|
|
11
|
+
attribute :context_after, :json, default: -> { {} }
|
|
12
|
+
|
|
13
|
+
# The foreign key guards the reference; validating it would load the
|
|
14
|
+
# execution again on every attempt the executor opens under the lock.
|
|
15
|
+
belongs_to :execution,
|
|
16
|
+
class_name: "Lepus::Saga::ActiveRecord::Execution",
|
|
17
|
+
foreign_key: :saga_execution_id,
|
|
18
|
+
inverse_of: :step_attempts,
|
|
19
|
+
optional: true
|
|
20
|
+
|
|
21
|
+
before_create { self.id ||= SecureRandom.uuid_v7 }
|
|
22
|
+
|
|
23
|
+
validates :step_name, presence: true
|
|
24
|
+
validates :status, inclusion: {in: STATUSES}
|
|
25
|
+
|
|
26
|
+
scope :chronological, -> { order(:started_at, :created_at) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lepus/saga"
|
|
4
|
+
require "active_record"
|
|
5
|
+
require_relative "active_record/version"
|
|
6
|
+
|
|
7
|
+
module Lepus
|
|
8
|
+
module Saga
|
|
9
|
+
# ActiveRecord models for the saga tables. Select the store with
|
|
10
|
+
#
|
|
11
|
+
# Lepus::Saga.configure { |config| config.store = :active_record }
|
|
12
|
+
module ActiveRecord
|
|
13
|
+
autoload :Record, "lepus/saga/active_record/record"
|
|
14
|
+
autoload :Execution, "lepus/saga/active_record/execution"
|
|
15
|
+
autoload :StepAttempt, "lepus/saga/active_record/step_attempt"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
require_relative "stores/active_record"
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lepus/saga/active_record"
|
|
4
|
+
|
|
5
|
+
module Lepus
|
|
6
|
+
module Saga
|
|
7
|
+
module Stores
|
|
8
|
+
# Executions and step attempts as ActiveRecord rows. Locks are
|
|
9
|
+
# SELECT ... FOR UPDATE inside a transaction (a no-op on SQLite, where
|
|
10
|
+
# ActiveRecord 8 opens IMMEDIATE transactions that serialize writers
|
|
11
|
+
# instead). Pass other model
|
|
12
|
+
# classes to use subclasses with their own table names or connections.
|
|
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` plucked values.
|
|
18
|
+
class ActiveRecord < 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::ActiveRecord::Execution
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def step_attempt_class
|
|
30
|
+
@step_attempt_class ||= Lepus::Saga::ActiveRecord::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.find_by(id: id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def save_execution(execution)
|
|
42
|
+
execution.save!
|
|
43
|
+
execution
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_lock(execution)
|
|
47
|
+
execution.with_lock { yield execution }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_locked_execution(id)
|
|
51
|
+
execution_class.transaction do
|
|
52
|
+
yield execution_class.lock.find_by(id: id)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def increment_resume_count(execution)
|
|
57
|
+
execution_class.update_counters(execution.id, resume_count: 1, touch: true)
|
|
58
|
+
execution.resume_count = execution_class.where(id: execution.id).pick(:resume_count)
|
|
59
|
+
execution.clear_attribute_changes(%i[resume_count])
|
|
60
|
+
execution.resume_count
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def stalled_executions(before:)
|
|
64
|
+
execution_class.in_progress.where(last_progress_at: ...before).order(:last_progress_at)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def executions_for_resource(resource_type, resource_id)
|
|
68
|
+
execution_class.for_resource(resource_type, resource_id).recent_first
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_step_attempt(execution, attributes)
|
|
72
|
+
step_attempt_class.create!(attributes.merge(saga_execution_id: execution.id))
|
|
73
|
+
rescue ::ActiveRecord::RecordNotUnique => 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
|
|
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
|
+
.exists?(started_at: started_since..)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def stale_step_attempts(execution, step_name:, direction:, epoch:, started_before:)
|
|
97
|
+
attempts_of(execution)
|
|
98
|
+
.where(step_name: step_name, direction: direction, epoch: epoch, status: "running", started_at: ...started_before)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def step_succeeded?(execution, step_name)
|
|
102
|
+
attempts_of(execution).exists?(step_name: step_name, direction: "execute", status: "succeeded")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def queries? = true
|
|
106
|
+
|
|
107
|
+
def list_executions(before_id: nil, limit: 50, **filters)
|
|
108
|
+
scope = matching_executions(**filters)
|
|
109
|
+
scope = scope.where(id: ...before_id) if before_id
|
|
110
|
+
scope.order(id: :desc).limit(limit).to_a
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def count_executions(group_by:, **filters)
|
|
114
|
+
keys = group_keys(group_by)
|
|
115
|
+
columns = keys.include?(:partition) ? keys | [:saga_name] : keys
|
|
116
|
+
counts = matching_executions(**filters).group(*columns).count
|
|
117
|
+
rows = counts.map { |values, count| [columns.zip((columns.size == 1) ? [values] : values).to_h, count] }
|
|
118
|
+
tally_groups(rows, group_by)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def execution_series(from:, to:, bucket:, saga_name: nil, max_rows: QUERY_MAX_ROWS)
|
|
122
|
+
seconds = series_bucket(from, to, bucket)
|
|
123
|
+
scope = matching_executions(saga_name: saga_name)
|
|
124
|
+
started, started_truncated = take_rows(
|
|
125
|
+
scope.where(created_at: from...to).order(created_at: :desc).limit(max_rows + 1).pluck(:created_at),
|
|
126
|
+
max_rows
|
|
127
|
+
)
|
|
128
|
+
finished, finished_truncated = take_rows(
|
|
129
|
+
scope.where(status: ExecutionBehaviour::TERMINAL_STATUSES, finished_at: from...to)
|
|
130
|
+
.order(finished_at: :desc).limit(max_rows + 1).pluck(:finished_at, :status),
|
|
131
|
+
max_rows
|
|
132
|
+
)
|
|
133
|
+
{rows: build_series(from, to, seconds, started, finished), truncated: started_truncated || finished_truncated}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def step_latency(from:, to:, saga_name: nil, max_rows: QUERY_MAX_ROWS)
|
|
137
|
+
attempts = step_attempt_class.arel_table
|
|
138
|
+
samples, truncated = take_rows(
|
|
139
|
+
attempts_with_saga(from, to, saga_name)
|
|
140
|
+
.where(status: LATENCY_STATUSES).where.not(duration_ms: nil)
|
|
141
|
+
.order(attempts[:started_at].desc).limit(max_rows + 1)
|
|
142
|
+
.pluck(execution_class.arel_table[:saga_name], attempts[:step_name], attempts[:direction], attempts[:duration_ms]),
|
|
143
|
+
max_rows
|
|
144
|
+
)
|
|
145
|
+
{rows: latency_rows(samples), truncated: truncated}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def failure_summary(from:, to:, saga_name: nil, limit: 20)
|
|
149
|
+
attempts = step_attempt_class.arel_table
|
|
150
|
+
saga = execution_class.arel_table[:saga_name]
|
|
151
|
+
failed = attempts_with_saga(from, to, saga_name).where(status: "failed")
|
|
152
|
+
failed
|
|
153
|
+
.group(attempts[:error_class], saga, attempts[:step_name])
|
|
154
|
+
.order(Arel::Nodes::Descending.new(Arel.star.count), Arel::Nodes::Descending.new(attempts[:finished_at].maximum))
|
|
155
|
+
.limit(limit)
|
|
156
|
+
.pluck(attempts[:error_class], saga, attempts[:step_name], Arel.star.count, attempts[:saga_execution_id].count(true))
|
|
157
|
+
.map do |error_class, group_saga, step_name, count, execution_count|
|
|
158
|
+
message, last_seen_at, execution_id = failed
|
|
159
|
+
.where(error_class: error_class, step_name: step_name).where(saga.eq(group_saga))
|
|
160
|
+
.order(attempts[:finished_at].desc, attempts[:id].desc)
|
|
161
|
+
.pick(attempts[:error_message], attempts[:finished_at], attempts[:saga_execution_id])
|
|
162
|
+
{
|
|
163
|
+
error_class: error_class, saga_name: group_saga, step_name: step_name, count: count,
|
|
164
|
+
execution_count: execution_count, last_seen_at: last_seen_at, sample_message: message, sample_execution_id: execution_id
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def saga_names
|
|
170
|
+
execution_class.distinct.order(:saga_name).pluck(:saga_name)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def attempts_of(execution)
|
|
176
|
+
step_attempt_class.where(saga_execution_id: execution.id)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def matching_executions(saga_name: nil, status: nil, current_step: nil, partition: nil, resource: nil, from: nil, to: nil,
|
|
180
|
+
progress_from: nil, progress_to: nil, error_class: nil, error_step: nil, error_from: nil, error_to: nil)
|
|
181
|
+
sagas = filter_values(saga_name)
|
|
182
|
+
statuses = filter_values(status)
|
|
183
|
+
steps = filter_values(current_step)
|
|
184
|
+
scope = execution_class.all
|
|
185
|
+
scope = scope.where(saga_name: sagas) if sagas
|
|
186
|
+
scope = scope.where(status: statuses) if statuses
|
|
187
|
+
scope = scope.where(current_step: steps) if steps
|
|
188
|
+
scope = scope.where(created_at: from..) if from
|
|
189
|
+
scope = scope.where(created_at: ...to) if to
|
|
190
|
+
scope = scope.where(last_progress_at: progress_from..) if progress_from
|
|
191
|
+
scope = scope.where(last_progress_at: ...progress_to) if progress_to
|
|
192
|
+
failing = failed_attempts(filter_values(error_class), filter_values(error_step), error_from, error_to)
|
|
193
|
+
scope = scope.where(id: failing.select(:saga_execution_id)) if failing
|
|
194
|
+
scope = scope.and(in_partition(partition.to_s)) if partition.present?
|
|
195
|
+
scope = scope.and(matching_resource(resource.to_s.downcase)) if resource.present?
|
|
196
|
+
scope
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def failed_attempts(classes, steps, from, to)
|
|
200
|
+
return if [classes, steps, from, to].all?(&:nil?)
|
|
201
|
+
|
|
202
|
+
scope = step_attempt_class.where(status: "failed")
|
|
203
|
+
scope = scope.where(error_class: classes) if classes
|
|
204
|
+
scope = scope.where(step_name: steps) if steps
|
|
205
|
+
scope = scope.where(started_at: from..) if from
|
|
206
|
+
scope = scope.where(started_at: ...to) if to
|
|
207
|
+
scope
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def in_partition(partition)
|
|
211
|
+
execution_class.where(partition: partition)
|
|
212
|
+
.or(execution_class.where(partition: nil, saga_name: partition_saga_names(partition)))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def matching_resource(needle)
|
|
216
|
+
execution_class.where(
|
|
217
|
+
"LOWER(resource_type) LIKE :pattern ESCAPE '!' OR LOWER(resource_id) LIKE :pattern ESCAPE '!'",
|
|
218
|
+
pattern: "%#{execution_class.sanitize_sql_like(needle, "!")}%"
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Attempts started in the window, joined to their execution.
|
|
223
|
+
def attempts_with_saga(from, to, saga_name)
|
|
224
|
+
attempts = step_attempt_class.arel_table
|
|
225
|
+
executions = execution_class.arel_table
|
|
226
|
+
scope = step_attempt_class
|
|
227
|
+
.joins(attempts.join(executions).on(executions[:id].eq(attempts[:saga_execution_id])).join_sources)
|
|
228
|
+
.where(started_at: from...to)
|
|
229
|
+
sagas = filter_values(saga_name)
|
|
230
|
+
sagas ? scope.where(executions[:saga_name].in(sagas)) : scope
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
data/mise.toml
ADDED
metadata
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lepus-saga-active_record
|
|
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: activerecord
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '8.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: activejob
|
|
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: pry
|
|
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: railties
|
|
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: rake
|
|
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: rspec
|
|
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
|
|
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: rubocop-performance
|
|
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
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: rubocop-rspec
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - ">="
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '0'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '0'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: standard
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - ">="
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '0'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - ">="
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '0'
|
|
166
|
+
description: Keeps lepus-saga executions and step attempts in ActiveRecord tables
|
|
167
|
+
(PostgreSQL, MySQL/MariaDB, SQLite), with a Rails generator for the migration
|
|
168
|
+
email:
|
|
169
|
+
- mgzmaster@gmail.com
|
|
170
|
+
executables: []
|
|
171
|
+
extensions: []
|
|
172
|
+
extra_rdoc_files: []
|
|
173
|
+
files:
|
|
174
|
+
- CHANGELOG.md
|
|
175
|
+
- Gemfile
|
|
176
|
+
- LICENSE.txt
|
|
177
|
+
- README.md
|
|
178
|
+
- Rakefile
|
|
179
|
+
- docs/brand/BRAND.md
|
|
180
|
+
- docs/brand/apple-touch-icon.png
|
|
181
|
+
- docs/brand/favicon-16.png
|
|
182
|
+
- docs/brand/favicon-32.png
|
|
183
|
+
- docs/brand/favicon.ico
|
|
184
|
+
- docs/brand/favicon.svg
|
|
185
|
+
- docs/brand/icon-512.png
|
|
186
|
+
- docs/brand/lockup-lepus-mono.svg
|
|
187
|
+
- docs/brand/lockup-lepus-on-dark.svg
|
|
188
|
+
- docs/brand/lockup-lepus-on-light.svg
|
|
189
|
+
- docs/brand/lockup-lepus-saga-mono.svg
|
|
190
|
+
- docs/brand/lockup-lepus-saga-on-dark.svg
|
|
191
|
+
- docs/brand/lockup-lepus-saga-on-light.svg
|
|
192
|
+
- docs/brand/lockup-lepus-saga.svg
|
|
193
|
+
- docs/brand/lockup-lepus.svg
|
|
194
|
+
- docs/brand/mark-mono.svg
|
|
195
|
+
- docs/brand/mark-on-dark.svg
|
|
196
|
+
- docs/brand/mark-on-light.svg
|
|
197
|
+
- docs/brand/mark-small-mono.svg
|
|
198
|
+
- docs/brand/mark-small-on-dark.svg
|
|
199
|
+
- docs/brand/mark-small-on-light.svg
|
|
200
|
+
- docs/brand/mark-small.svg
|
|
201
|
+
- docs/brand/mark.svg
|
|
202
|
+
- lib/generators/lepus/saga/dashboard_indexes/dashboard_indexes_generator.rb
|
|
203
|
+
- lib/generators/lepus/saga/dashboard_indexes/templates/add_lepus_saga_dashboard_indexes.rb.erb
|
|
204
|
+
- lib/generators/lepus/saga/install/install_generator.rb
|
|
205
|
+
- lib/generators/lepus/saga/install/templates/create_lepus_saga_tables.rb.erb
|
|
206
|
+
- lib/lepus/saga/active_record.rb
|
|
207
|
+
- lib/lepus/saga/active_record/execution.rb
|
|
208
|
+
- lib/lepus/saga/active_record/record.rb
|
|
209
|
+
- lib/lepus/saga/active_record/step_attempt.rb
|
|
210
|
+
- lib/lepus/saga/active_record/version.rb
|
|
211
|
+
- lib/lepus/saga/stores/active_record.rb
|
|
212
|
+
- mise.toml
|
|
213
|
+
homepage: https://github.com/marcosgz/lepus-saga-active_record
|
|
214
|
+
licenses:
|
|
215
|
+
- MIT
|
|
216
|
+
metadata:
|
|
217
|
+
homepage_uri: https://github.com/marcosgz/lepus-saga-active_record
|
|
218
|
+
source_code_uri: https://github.com/marcosgz/lepus-saga-active_record
|
|
219
|
+
changelog_uri: https://github.com/marcosgz/lepus-saga-active_record/blob/master/CHANGELOG.md
|
|
220
|
+
rdoc_options: []
|
|
221
|
+
require_paths:
|
|
222
|
+
- lib
|
|
223
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
224
|
+
requirements:
|
|
225
|
+
- - ">="
|
|
226
|
+
- !ruby/object:Gem::Version
|
|
227
|
+
version: 3.3.0
|
|
228
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
229
|
+
requirements:
|
|
230
|
+
- - ">="
|
|
231
|
+
- !ruby/object:Gem::Version
|
|
232
|
+
version: '0'
|
|
233
|
+
requirements: []
|
|
234
|
+
rubygems_version: 3.6.9
|
|
235
|
+
specification_version: 4
|
|
236
|
+
summary: ActiveRecord store for Lepus::Saga
|
|
237
|
+
test_files: []
|