pavedb 0.9.0__py3-none-any.whl

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.
pave/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ # (C) 2025 Rodrigo Rodrigues da Silva <rodrigo@flowlexi.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+ __all__ = [
4
+ "config", "auth", "preprocess", "metrics", "service", "main", "cli",
5
+ "embedders", "stores", "log",
6
+ ]
@@ -0,0 +1,4 @@
1
+ # (C) 2025 Rodrigo Rodrigues da Silva <rodrigo@flowlexi.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ # pkg
@@ -0,0 +1,229 @@
1
+ # PaveDB sample configuration
2
+ # Copy to config.yml and adjust.
3
+ # Secrets (API keys) should live in a separate untracked file — see tenants.yml.example.
4
+ #
5
+ # Default user-install paths:
6
+ # config: ~/pavedb/config.yml
7
+ # tenants: ~/pavedb/tenants.yml
8
+ # data: ~/pavedb/data
9
+ #
10
+ # Distro-like install example:
11
+ # config: /etc/pavedb/config.yml
12
+ # tenants: /var/pavedb/tenants.yml
13
+ # data: /var/pavedb/data
14
+ #
15
+ # Config file location override:
16
+ # PAVEDB_CONFIG=/etc/pavedb/config.yml
17
+ #
18
+ # In Docker/systemd deployments always set PAVEDB_CONFIG explicitly — the
19
+ # default path expands ~ relative to the process user, which may not be what
20
+ # you expect inside a container. Example compose snippet:
21
+ #
22
+ # environment:
23
+ # PAVEDB_CONFIG: /etc/pavedb/config.yml
24
+ # volumes:
25
+ # - ./config.yml:/etc/pavedb/config.yml:ro
26
+ #
27
+ # All keys can also be overridden inline via environment variables:
28
+ # PAVEDB_<KEY>=value (top-level, e.g. PAVEDB_DATA_DIR)
29
+ # PAVEDB_<SECTION>__<KEY>=val (nested, e.g. PAVEDB_LOG__LEVEL=debug)
30
+ # `auth.tenants_file` is optional. If unset, its default is `None` and no
31
+ # tenants sidecar file is loaded.
32
+ # If set, PaveDB loads that sidecar first. Then inline tenant config is
33
+ # applied with precedence:
34
+ # env vars > config.yml > tenants.yml > defaults
35
+ # Example: define tenant "acme" entirely from env:
36
+ # PAVEDB_AUTH__API_KEYS__acme=change-me
37
+ # PAVEDB_TENANTS__acme__MAX_CONCURRENT=5
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Storage
41
+ # ---------------------------------------------------------------------------
42
+
43
+ # Data directory — ~ is expanded at startup.
44
+ # Default (library/dev): ~/pavedb/data
45
+ # For distro-like installs use an absolute path, e.g. /var/pavedb/data.
46
+ data_dir: ~/pavedb/data
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Common collection
50
+ # ---------------------------------------------------------------------------
51
+ # When enabled, every tenant search also queries a shared "common" collection
52
+ # and merges the results. Useful for org-wide reference data (e.g. a shared
53
+ # code/term dictionary) that should be visible to all tenants.
54
+ # The common collection is owned by common_tenant and is never rate-limited.
55
+
56
+ common_enabled: false
57
+ common_tenant: global # tenant that owns the common collection
58
+ common_collection: common # collection name within that tenant
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Authentication
62
+ # ---------------------------------------------------------------------------
63
+
64
+ auth:
65
+ # none — no auth; all requests accepted. Dev/private deployments only.
66
+ # enforce_policy() will reject auth=none on non-loopback interfaces.
67
+ # static — Bearer token per tenant; keys defined below or in tenants_file.
68
+ mode: static
69
+
70
+ # Default tenant name when auth.mode=none (no key required).
71
+ default_access_tenant: public
72
+
73
+ # Global admin key — grants access to all tenants and admin routes.
74
+ # Always read from the environment; never hardcode in committed files.
75
+ global_key: ${PAVEDB_GLOBAL_KEY}
76
+
77
+ # External tenant→key mapping file.
78
+ # Keep the same key paths there (`auth.api_keys`, `tenants.*`).
79
+ # User install default: ~/pavedb/tenants.yml
80
+ # Distro-like install: /var/pavedb/tenants.yml
81
+ # tenants_file: ~/pavedb/tenants.yml
82
+
83
+ # Inline tenant→key mapping (fallback; keep empty in the repo).
84
+ api_keys: {}
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Vector store
88
+ # ---------------------------------------------------------------------------
89
+
90
+ vector_store:
91
+ # faiss — local FAISS index (built-in, no extra services required).
92
+ type: faiss
93
+
94
+ # Options for type=faiss.
95
+ faiss:
96
+ embed_model: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
97
+ max_query_chars: 4000
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Embedder
101
+ # ---------------------------------------------------------------------------
102
+ # Controls how text is converted to vectors before indexing and querying.
103
+ # The embedder is shared across all collections (per-collection config: v0.6).
104
+
105
+ embedder:
106
+ # sbert — direct sentence-transformers (recommended local default).
107
+ # openai — OpenAI embeddings API (requires API key, adds latency).
108
+ type: sbert
109
+
110
+ # Used by type=sbert.
111
+ sbert:
112
+ runtime: auto # auto | process | direct
113
+ model: sentence-transformers/all-MiniLM-L6-v2
114
+ batch_size: 64
115
+ device: auto # cpu | cuda | mps | auto
116
+
117
+ # Used by type=openai.
118
+ openai:
119
+ model: text-embedding-3-small
120
+ batch_size: 256
121
+ api_key: ${PAVEDB_OPENAI_API_KEY}
122
+ dim: 1536
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Ingest limits
126
+ # ---------------------------------------------------------------------------
127
+
128
+ ingest:
129
+ # Reject uploads larger than this (MB). 0 = unlimited.
130
+ # Also set your reverse proxy: e.g. nginx client_max_body_size >= this value.
131
+ max_file_size_mb: 500
132
+
133
+ # Max parallel ingests; excess requests get 503 immediately.
134
+ # Ingest is CPU/memory heavy — keep this well below your core count.
135
+ # In-code default is 7 (conservative floor); 10 below is a typical-dev-host
136
+ # value. Rule of thumb: ~1× core count on dedicated hosts (e.g. 10 on a
137
+ # 10-core M-series, 16 on a 16-vCPU VM).
138
+ # 0 = unlimited (not recommended in production).
139
+ max_concurrent: 10
140
+
141
+ # Reverse-proxy timeout guidance for ingest endpoints:
142
+ # Ingest embeds the entire document before responding; large files can take
143
+ # tens of seconds. Raise these nginx directives accordingly:
144
+ #
145
+ # proxy_read_timeout 300; # wait up to 5 min for upstream response
146
+ # client_body_timeout 120; # wait up to 2 min for client to send body
147
+ #
148
+ # Your HTTP client read timeout should also be at least 300 s for ingest.
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # Search limits
152
+ # ---------------------------------------------------------------------------
153
+
154
+ search:
155
+ # Max parallel searches; excess requests get 503 immediately.
156
+ # In-code default is 42 (conservative floor); 48 below is a typical-dev-host
157
+ # value. Rule of thumb: ~4–5× core count on dedicated hosts; throughput
158
+ # plateaus past that and only latency grows.
159
+ # 0 = unlimited (not recommended in production).
160
+ max_concurrent: 48
161
+
162
+ # Per-search timeout in ms; returns 503 on expiry. 0 = no timeout.
163
+ timeout_ms: 30000
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # Per-tenant concurrency limits
167
+ # ---------------------------------------------------------------------------
168
+ # Applies to all tenant-scoped routes (search, ingest, collection ops).
169
+ # Admin requests and global-key requests bypass these limits.
170
+ # Per-tenant overrides can be set in tenants.yml (see tenants.yml.example).
171
+ # Moving-window limits (max_rpm, max_rph) require SQLite — post-v0.5.8.
172
+
173
+ tenants:
174
+ default_max_concurrent: 10 # applies to every tenant; 0 = unlimited
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Text preprocessing
178
+ # ---------------------------------------------------------------------------
179
+ # Controls how plain-text (.txt) files are split into chunks before embedding.
180
+ # Overlap ensures context is preserved across chunk boundaries.
181
+
182
+ preprocess:
183
+ txt_chunk_size: 1000 # characters per chunk
184
+ txt_chunk_overlap: 200 # characters of overlap between adjacent chunks
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Server
188
+ # ---------------------------------------------------------------------------
189
+ # These defaults are overridden by env vars HOST / PORT / RELOAD / WORKERS.
190
+
191
+ server:
192
+ host: 0.0.0.0
193
+ port: 8086
194
+ reload: false
195
+ workers: 1
196
+
197
+ # Keep-alive timeout in seconds (passed to uvicorn --timeout-keep-alive).
198
+ # Must be lower than your reverse proxy's keepalive_timeout.
199
+ # nginx default is 75 s — set this to 65 when behind nginx.
200
+ timeout_keep_alive: 75
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # Logging
204
+ # ---------------------------------------------------------------------------
205
+
206
+ log:
207
+ # Dev log level (stderr). DEBUG | INFO | WARNING | ERROR — default INFO.
208
+ # Overridden by PAVEDB_LOG__LEVEL env var (e.g. in Makefile: debug).
209
+ # Per-namespace overrides: log.debug / log.watch / log.quiet (list of loggers).
210
+ level: INFO
211
+
212
+ # Ops log — one structured JSON line per operation (search, ingest, delete…).
213
+ # Fields: ts, op, tenant, collection, latency_ms, status, error_code, hits.
214
+ # null (off) | stdout | /path/to/ops.jsonl
215
+ # stdout is recommended for Docker/12-factor; pave uses stderr for the dev log.
216
+ # File paths suit traditional deployments; no rotation is provided here.
217
+ ops_log: null
218
+
219
+ # Uvicorn access log destination.
220
+ # null (use uvicorn default) | stdout | /path/to/access.log
221
+ access_log: null
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # Instance branding (optional)
225
+ # ---------------------------------------------------------------------------
226
+
227
+ instance:
228
+ name: PaveDB
229
+ desc: Vector Search Microservice (pluggable, functional)
Binary file
Binary file
@@ -0,0 +1,19 @@
1
+ # Untracked secrets file with tenant API keys and per-tenant limits.
2
+ # Name it 'tenants.yml', adjust and DO NOT commit.
3
+ # The installed default should be innocuous; uncomment the examples you need.
4
+ #
5
+ # auth:
6
+ # api_keys:
7
+ # acme: change-me
8
+ # umbrella: change-me
9
+
10
+ # Per-tenant concurrency overrides.
11
+ # Omit a tenant entry to fall back to tenants.default_max_concurrent in config.yml.
12
+ # 0 = unlimited for that tenant.
13
+ # Moving-window limits (max_rpm, max_rph) will be added here after SQLite lands.
14
+ #
15
+ # tenants:
16
+ # acme:
17
+ # max_concurrent: 5 # overrides global default for acme; 0 = unlimited
18
+ # umbrella:
19
+ # max_concurrent: 20
pave/assets/ui.html ADDED
@@ -0,0 +1,366 @@
1
+ <!doctype html>
2
+ <!-- (C) 2025 Rodrigo Rodrigues da Silva <rodrigo@flowlexi.com> -->
3
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
4
+
5
+ <html lang="en">
6
+ <head>
7
+ <meta charset="utf-8" />
8
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
9
+ <!-- favicon robusto -->
10
+ <link rel="icon" type="image/png" sizes="32x32"
11
+ href="/assets/pavedb_icon_192.png?v=__VERSION__" />
12
+ <link rel="icon" type="image/png" sizes="192x192"
13
+ href="/assets/pavedb_icon_192.png?v=__VERSION__" />
14
+ <link rel="icon" href="/favicon.ico?v=__VERSION__" />
15
+ <title>__INST_NAME__ • Search</title>
16
+ <style>
17
+ /* Flowlexi docs palette with restrained PaveDB accents */
18
+ :root{
19
+ --bg:#F4F2ED; --panel:#FFFFFF; --text:#1E1E1E; --muted:#4B5563;
20
+ --accent:#2ECC71; --border:#E5E7EB; --link:#11957D;
21
+ --link-accent:#0B7769;
22
+ }
23
+
24
+ *{ box-sizing:border-box }
25
+ html,body{ height:100% }
26
+ body{
27
+ min-height:100vh; display:flex; flex-direction:column; overflow:hidden;
28
+ margin:0; font:16px/1.45 system-ui,Segoe UI,Roboto,Inter,Arial;
29
+ background:var(--bg); color:var(--text);
30
+ }
31
+
32
+ .bar{
33
+ flex:0 0 auto; display:flex; gap:10px; align-items:center;
34
+ padding:12px; border-bottom:1px solid var(--border); background:var(--bg);
35
+ }
36
+ .left{ display:flex; gap:8px; align-items:center }
37
+ .right{
38
+ margin-left:auto; color:var(--muted); white-space:nowrap;
39
+ overflow:hidden; text-overflow:ellipsis;
40
+ }
41
+
42
+ .tab{
43
+ padding:8px 12px; border-radius:10px; border:1px solid var(--border);
44
+ cursor:pointer; background:var(--panel); color:var(--text);
45
+ }
46
+ .tab:hover{
47
+ background:#F1F3F4; color:var(--link-accent);
48
+ }
49
+ .tab.active{
50
+ background:#D9E3E2; color:var(--link-accent);
51
+ border-color:var(--link-accent);
52
+ }
53
+
54
+ .frames{ flex:1 1 auto; min-height:0; background:var(--bg) }
55
+ .frame{ display:none; width:100%; height:100%; border:0; background:var(--bg) }
56
+ .frame.active{ display:block }
57
+
58
+ .footer{
59
+ flex:0 0 auto; position:sticky; bottom:0;
60
+ display:flex; gap:12px; align-items:center; justify-content:center;
61
+ padding:10px; color:var(--muted); border-top:1px solid var(--border);
62
+ background:var(--bg);
63
+ }
64
+ .footer a{ color:var(--link); text-decoration:none }
65
+ .footer a:hover{ color:var(--link-accent) }
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <div class="bar">
70
+ <div class="left">
71
+ <button class="tab active" data-target="search"
72
+ data-title="__INST_NAME__ • Search">Search</button>
73
+ <button class="tab" data-target="data"
74
+ data-title="__INST_NAME__ • Data">Data</button>
75
+ <button class="tab" data-target="admin"
76
+ data-title="__INST_NAME__ • Admin">Admin</button>
77
+ </div>
78
+ <div class="right">
79
+ <strong>__INST_NAME__</strong> — <small>__INST_DESC__</small>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="frames">
84
+ <iframe id="search" class="frame active" src="/ui/search" title="Search"></iframe>
85
+ <iframe id="data" class="frame" src="/ui/data" title="Data"></iframe>
86
+ <iframe id="admin" class="frame" src="/ui/admin" title="Admin"></iframe>
87
+ </div>
88
+
89
+ <div class="footer">
90
+ © 2025, 2026
91
+ <a href="https://flowlexi.com" target="_blank"
92
+ rel="noopener">Flowlexi</a> •
93
+ <a href="__REPO_URL__" target="_blank"
94
+ rel="noopener">🛣️ PaveDB __VERSION__</a> •
95
+ <a href="__LICENSE_URL__" target="_blank" rel="noopener">__LICENSE_NAME__</a>
96
+ </div>
97
+ <script>
98
+ function activeFrame(){ return document.querySelector('.frame.active'); }
99
+
100
+ function scrubSwagger(frame){
101
+ const win = frame?.contentWindow;
102
+ const doc = win?.document;
103
+ if(!doc) return;
104
+ if(!doc.querySelector('.swagger-ui')){
105
+ setTimeout(()=>scrubSwagger(frame), 80);
106
+ return;
107
+ }
108
+
109
+ // CSS: remove topo/infos
110
+ const STYLE_ID = 'pv-clean';
111
+ if(!doc.getElementById(STYLE_ID)){
112
+ const s = doc.createElement('style');
113
+ s.id = STYLE_ID;
114
+ s.textContent = `
115
+ :root{
116
+ --bg:#F4F2ED; --panel:#FFFFFF; --text:#1E1E1E;
117
+ --muted:#4B5563; --accent:#2ECC71; --border:#E5E7EB;
118
+ --link:#11957D; --link-accent:#0B7769;
119
+ }
120
+ #operations-tag-default,
121
+ .swagger-ui .topbar,
122
+ .swagger-ui .download-url-wrapper,
123
+ .swagger-ui .information-container,
124
+ .swagger-ui .info,
125
+ .swagger-ui .servers { display:none !important; }
126
+ html, body {
127
+ background: var(--bg) !important;
128
+ color: var(--text) !important;
129
+ }
130
+ .swagger-ui {
131
+ color: var(--text) !important;
132
+ }
133
+ .swagger-ui .wrapper {
134
+ margin:0 !important; padding:0 0 8px !important;
135
+ max-width:none !important;
136
+ }
137
+ .swagger-ui a,
138
+ .swagger-ui .opblock-summary-path,
139
+ .swagger-ui .link {
140
+ color: var(--link) !important;
141
+ }
142
+ .swagger-ui a:hover,
143
+ .swagger-ui .opblock-summary-path:hover,
144
+ .swagger-ui .link:hover {
145
+ color: var(--link-accent) !important;
146
+ }
147
+ .swagger-ui section.models,
148
+ .swagger-ui .model-box {
149
+ background: var(--panel) !important;
150
+ border:1px solid var(--border) !important;
151
+ box-shadow:none !important;
152
+ }
153
+ .swagger-ui .scheme-container {
154
+ margin:8px 0 0 !important;
155
+ padding:0 16px 8px !important;
156
+ background: transparent !important;
157
+ border:0 !important;
158
+ box-shadow:none !important;
159
+ }
160
+ .swagger-ui .scheme-container .schemes,
161
+ .swagger-ui .scheme-container .auth-wrapper {
162
+ background: transparent !important;
163
+ border:0 !important;
164
+ box-shadow:none !important;
165
+ }
166
+ .swagger-ui .opblock {
167
+ background: var(--panel) !important;
168
+ border:1px solid var(--border) !important;
169
+ box-shadow:none !important;
170
+ }
171
+ .swagger-ui .opblock .opblock-summary,
172
+ .swagger-ui .responses-inner,
173
+ .swagger-ui table tbody tr td,
174
+ .swagger-ui .parameters-col_description input,
175
+ .swagger-ui .parameters-col_description textarea,
176
+ .swagger-ui .model-example {
177
+ border-color: var(--border) !important;
178
+ }
179
+ .swagger-ui .opblock-tag,
180
+ .swagger-ui .opblock-summary-description,
181
+ .swagger-ui .response-col_description__inner p,
182
+ .swagger-ui .parameter__type,
183
+ .swagger-ui .parameter__name,
184
+ .swagger-ui .responses-inner h4,
185
+ .swagger-ui .responses-inner h5,
186
+ .swagger-ui .model-title,
187
+ .swagger-ui label,
188
+ .swagger-ui .prop-name {
189
+ color: var(--text) !important;
190
+ }
191
+ .swagger-ui .opblock-description-wrapper p,
192
+ .swagger-ui .response-col_links,
193
+ .swagger-ui .responses-inner .markdown p,
194
+ .swagger-ui .renderedMarkdown p,
195
+ .swagger-ui .parameter__deprecated,
196
+ .swagger-ui .tab li,
197
+ .swagger-ui .model-title small,
198
+ .swagger-ui .prop-type {
199
+ color: var(--muted) !important;
200
+ }
201
+ .swagger-ui .copy-to-clipboard,
202
+ .swagger-ui .model-hint {
203
+ color: #6B7280 !important;
204
+ }
205
+ .swagger-ui .opblock-body pre.microlight,
206
+ .swagger-ui .highlight-code,
207
+ .swagger-ui textarea,
208
+ .swagger-ui input[type=text],
209
+ .swagger-ui input[type=password],
210
+ .swagger-ui select {
211
+ background: #F1F3F4 !important;
212
+ border:1px solid var(--border) !important;
213
+ color: var(--text) !important;
214
+ box-shadow:none !important;
215
+ }
216
+ .swagger-ui textarea:focus,
217
+ .swagger-ui input[type=text]:focus,
218
+ .swagger-ui input[type=password]:focus,
219
+ .swagger-ui select:focus {
220
+ border-color: var(--link) !important;
221
+ outline:none !important;
222
+ }
223
+ .swagger-ui .tab li.active {
224
+ background: #D9E3E2 !important;
225
+ color: var(--text) !important;
226
+ }
227
+ .swagger-ui .btn {
228
+ border-radius:8px !important;
229
+ box-shadow:none !important;
230
+ }
231
+ .swagger-ui .btn.authorize,
232
+ .swagger-ui .btn.execute,
233
+ .swagger-ui .btn.try-out__btn {
234
+ background: var(--link) !important;
235
+ border-color: var(--link) !important;
236
+ color: #FFFFFF !important;
237
+ }
238
+ .swagger-ui .btn.authorize:hover,
239
+ .swagger-ui .btn.execute:hover,
240
+ .swagger-ui .btn.try-out__btn:hover,
241
+ .swagger-ui .btn.authorize:focus,
242
+ .swagger-ui .btn.execute:focus,
243
+ .swagger-ui .btn.try-out__btn:focus {
244
+ background: var(--link-accent) !important;
245
+ border-color: var(--link-accent) !important;
246
+ color: #FFFFFF !important;
247
+ }
248
+ .swagger-ui .btn.cancel {
249
+ background: var(--panel) !important;
250
+ border-color: var(--border) !important;
251
+ color: var(--muted) !important;
252
+ }
253
+ .swagger-ui .btn.cancel:hover,
254
+ .swagger-ui .btn.cancel:focus {
255
+ background: #F1F3F4 !important;
256
+ color: var(--text) !important;
257
+ }
258
+ .swagger-ui .btn.authorize svg,
259
+ .swagger-ui .btn.execute svg {
260
+ fill: currentColor !important;
261
+ }
262
+ .swagger-ui .opblock.opblock-get .opblock-summary {
263
+ background: rgba(91,141,184,.08) !important;
264
+ }
265
+ .swagger-ui .opblock.opblock-post .opblock-summary {
266
+ background: rgba(17,149,125,.08) !important;
267
+ }
268
+ .swagger-ui .opblock.opblock-put .opblock-summary {
269
+ background: rgba(201,130,58,.08) !important;
270
+ }
271
+ .swagger-ui .opblock.opblock-patch .opblock-summary {
272
+ background: rgba(68,191,152,.08) !important;
273
+ }
274
+ .swagger-ui .opblock.opblock-delete .opblock-summary {
275
+ background: rgba(176,82,74,.08) !important;
276
+ }
277
+ .swagger-ui .opblock.opblock-head .opblock-summary {
278
+ background: rgba(122,107,152,.08) !important;
279
+ }
280
+ .swagger-ui .opblock.opblock-options .opblock-summary {
281
+ background: rgba(79,111,143,.08) !important;
282
+ }
283
+ .swagger-ui .opblock.opblock-get .opblock-summary-method {
284
+ background:#5B8DB8 !important;
285
+ }
286
+ .swagger-ui .opblock.opblock-post .opblock-summary-method {
287
+ background:#11957D !important;
288
+ }
289
+ .swagger-ui .opblock.opblock-put .opblock-summary-method {
290
+ background:#C9823A !important;
291
+ }
292
+ .swagger-ui .opblock.opblock-patch .opblock-summary-method {
293
+ background:#44BF98 !important;
294
+ }
295
+ .swagger-ui .opblock.opblock-delete .opblock-summary-method {
296
+ background:#B0524A !important;
297
+ }
298
+ .swagger-ui .opblock.opblock-head .opblock-summary-method {
299
+ background:#7A6B98 !important;
300
+ }
301
+ .swagger-ui .opblock.opblock-options .opblock-summary-method {
302
+ background:#4F6F8F !important;
303
+ }
304
+ `;
305
+ doc.head.appendChild(s);
306
+ }
307
+ }
308
+
309
+ const TAB_KEY = 'pavedb.ui.tab';
310
+ const tabs = document.querySelectorAll('.tab');
311
+ const frames = document.querySelectorAll('.frame');
312
+
313
+ function tabNames(){
314
+ return Array.from(tabs).map(function(tab){ return tab.dataset.target; });
315
+ }
316
+
317
+ function tabFromLocation(){
318
+ const qs = new URLSearchParams(window.location.search);
319
+ const queryTab = qs.get('tab');
320
+ if(queryTab) return queryTab;
321
+ const hash = window.location.hash.replace(/^#\/?/, '');
322
+ return hash || '';
323
+ }
324
+
325
+ function rememberedTab(){
326
+ try { return window.localStorage.getItem(TAB_KEY) || ''; }
327
+ catch (_err) { return ''; }
328
+ }
329
+
330
+ function writeTab(name){
331
+ try { window.localStorage.setItem(TAB_KEY, name); }
332
+ catch (_err) {}
333
+ const url = new URL(window.location.href);
334
+ url.searchParams.set('tab', name);
335
+ url.hash = '/' + name;
336
+ window.history.replaceState(null, '', url);
337
+ }
338
+
339
+ function activateTab(name, updateUrl){
340
+ if(tabNames().indexOf(name) === -1) name = 'search';
341
+ tabs.forEach(function(tab){ tab.classList.remove('active'); });
342
+ frames.forEach(function(frame){ frame.classList.remove('active'); });
343
+ const tab = document.querySelector('.tab[data-target="' + name + '"]');
344
+ const frame = document.getElementById(name);
345
+ if(tab) tab.classList.add('active');
346
+ if(frame) frame.classList.add('active');
347
+ if(tab && tab.dataset.title) document.title = tab.dataset.title;
348
+ if(frame) scrubSwagger(frame);
349
+ if(updateUrl) writeTab(name);
350
+ }
351
+
352
+ tabs.forEach(function(tab){
353
+ tab.addEventListener('click', function(){
354
+ activateTab(tab.dataset.target, true);
355
+ });
356
+ });
357
+
358
+ frames.forEach(function(frame){
359
+ frame.addEventListener('load', function(){ scrubSwagger(frame); });
360
+ });
361
+
362
+ const initialTab = tabFromLocation() || rememberedTab() || 'search';
363
+ activateTab(initialTab, false);
364
+ </script>
365
+ </body>
366
+ </html>