tep 0.11.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/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
data/lib/tep/job.rb
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Tep::Job -- sidekiq-shaped background jobs over a SQLite queue.
|
|
2
|
+
#
|
|
3
|
+
# Why a queue at all?
|
|
4
|
+
# -------------------
|
|
5
|
+
# Tep::Parallel covers synchronous fan-out within one request. Some
|
|
6
|
+
# work doesn't fit: it's too slow to inline (an LLM call), needs to
|
|
7
|
+
# survive the request lifetime (a follow-up email), or should run
|
|
8
|
+
# on a cron-like cadence (refresh a cached snapshot). For those,
|
|
9
|
+
# you want sidekiq's shape: enqueue from anywhere, a separate
|
|
10
|
+
# worker process drains the queue.
|
|
11
|
+
#
|
|
12
|
+
# Storage
|
|
13
|
+
# -------
|
|
14
|
+
# SQLite, in a table the framework creates on demand:
|
|
15
|
+
#
|
|
16
|
+
# CREATE TABLE tep_jobs (
|
|
17
|
+
# id INTEGER PRIMARY KEY,
|
|
18
|
+
# job_name TEXT, -- registered class identifier
|
|
19
|
+
# arg TEXT, -- single string payload
|
|
20
|
+
# status TEXT, -- queued|running|done|failed
|
|
21
|
+
# created_at INTEGER,
|
|
22
|
+
# finished_at INTEGER,
|
|
23
|
+
# result TEXT
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
# The single-arg payload is intentional: structured data goes
|
|
27
|
+
# through JSON (Tep::Json) which we already ship. Sidekiq's
|
|
28
|
+
# multi-arg `perform_async(a, b, c)` translates to encoding the
|
|
29
|
+
# tuple as a JSON string and decoding it in `perform`.
|
|
30
|
+
#
|
|
31
|
+
# API
|
|
32
|
+
# ---
|
|
33
|
+
# Define a job by subclassing Tep::Job and overriding `perform`:
|
|
34
|
+
#
|
|
35
|
+
# class HelloJob < Tep::Job
|
|
36
|
+
# def perform(arg)
|
|
37
|
+
# Tep::Logger.new.info("hello " + arg)
|
|
38
|
+
# "done"
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# Enqueue from anywhere:
|
|
43
|
+
#
|
|
44
|
+
# Tep::Job.enqueue("HelloJob", "world", DB_PATH)
|
|
45
|
+
#
|
|
46
|
+
# Worker side: fetch one, dispatch, mark done. The dispatch is
|
|
47
|
+
# user-written because spinel doesn't carry cls_id tags through
|
|
48
|
+
# `PtrArray<Tep::Job>`, so the framework can't virtual-dispatch
|
|
49
|
+
# `handler.perform(arg)` to the right subclass on its own.
|
|
50
|
+
#
|
|
51
|
+
# loop do
|
|
52
|
+
# claim = Tep::Job.fetch_next(DB_PATH) # "" if empty, else
|
|
53
|
+
# # "row_id|name|arg"
|
|
54
|
+
# break if claim.length == 0
|
|
55
|
+
# parts = claim.split("|", 3)
|
|
56
|
+
# row_id = parts[0].to_i
|
|
57
|
+
# name = parts[1]
|
|
58
|
+
# arg = parts[2]
|
|
59
|
+
# result = ""
|
|
60
|
+
# if name == "HelloJob"
|
|
61
|
+
# result = HelloJob.new.perform(arg)
|
|
62
|
+
# end
|
|
63
|
+
# Tep::Job.mark_done(DB_PATH, row_id, result)
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# The verbosity of the `if name == "..."` ladder is the price of
|
|
67
|
+
# type safety in spinel. A future bin/tep pass could generate this
|
|
68
|
+
# dispatcher from the set of `Tep::Job` subclasses at compile time
|
|
69
|
+
# (mirroring the way routes are generated from `get '/x' do .. end`),
|
|
70
|
+
# at which point this surface becomes a one-liner. For v0.5 the
|
|
71
|
+
# manual ladder is fine -- a single tep app rarely has more than
|
|
72
|
+
# a handful of distinct job classes.
|
|
73
|
+
#
|
|
74
|
+
# Comparison to sidekiq
|
|
75
|
+
# ---------------------
|
|
76
|
+
# Sidekiq's `MyJob.perform_async(x)` enqueues on a Redis list keyed
|
|
77
|
+
# by class name. We do the same with SQLite + an explicit name
|
|
78
|
+
# string. The `Tep::Job` subclass + `perform(arg)` shape stays;
|
|
79
|
+
# only the worker drain loop differs (sidekiq does the dispatch via
|
|
80
|
+
# Ruby's `Object.const_get`, which spinel can't lower).
|
|
81
|
+
module Tep
|
|
82
|
+
class Job
|
|
83
|
+
# Subclasses override. The default uses `arg` as :str so spinel's
|
|
84
|
+
# analyzer pins the param type rather than defaulting to :int
|
|
85
|
+
# for an unused parameter -- otherwise subclass `arg.upcase` calls
|
|
86
|
+
# fail to resolve against an int-typed slot.
|
|
87
|
+
def perform(arg)
|
|
88
|
+
"" + arg
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Idempotent. Creates the queue table if missing. Pass the same
|
|
92
|
+
# SQLite path to enqueue / fetch_next / mark_done.
|
|
93
|
+
def self.init_schema(db_path)
|
|
94
|
+
db = Tep::SQLite.new
|
|
95
|
+
if db.open(db_path)
|
|
96
|
+
db.exec("CREATE TABLE IF NOT EXISTS tep_jobs (" +
|
|
97
|
+
"id INTEGER PRIMARY KEY, " +
|
|
98
|
+
"job_name TEXT, arg TEXT, status TEXT, " +
|
|
99
|
+
"created_at INTEGER, finished_at INTEGER, result TEXT)")
|
|
100
|
+
db.close
|
|
101
|
+
end
|
|
102
|
+
0
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Append a `queued` row. Returns the new row id (0 on DB error).
|
|
106
|
+
def self.enqueue(name, arg, db_path)
|
|
107
|
+
db = Tep::SQLite.new
|
|
108
|
+
if !db.open(db_path)
|
|
109
|
+
return 0
|
|
110
|
+
end
|
|
111
|
+
db.prepare("INSERT INTO tep_jobs (job_name, arg, status, created_at) VALUES (?, ?, ?, ?)")
|
|
112
|
+
db.bind_str(1, name)
|
|
113
|
+
db.bind_str(2, arg)
|
|
114
|
+
db.bind_str(3, "queued")
|
|
115
|
+
db.bind_int(4, Time.now.to_i)
|
|
116
|
+
db.step
|
|
117
|
+
db.finalize
|
|
118
|
+
id = db.last_rowid
|
|
119
|
+
db.close
|
|
120
|
+
id
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Claim the oldest `queued` row and mark it `running`. Returns
|
|
124
|
+
# "row_id|name|arg" packed into one string (the caller splits on
|
|
125
|
+
# "|" with limit 3), or "" if the queue is empty / errored. The
|
|
126
|
+
# row_id is needed for the matching `mark_done` call. Caller is
|
|
127
|
+
# responsible for dispatching to the right subclass and then
|
|
128
|
+
# writing the result back via `mark_done`.
|
|
129
|
+
def self.fetch_next(db_path)
|
|
130
|
+
db = Tep::SQLite.new
|
|
131
|
+
if !db.open(db_path)
|
|
132
|
+
return ""
|
|
133
|
+
end
|
|
134
|
+
db.prepare("SELECT id, job_name, arg FROM tep_jobs WHERE status = 'queued' ORDER BY id ASC LIMIT 1")
|
|
135
|
+
out = ""
|
|
136
|
+
if db.step == 1
|
|
137
|
+
row_id = db.col_int(0)
|
|
138
|
+
job_name = db.col_str(1)
|
|
139
|
+
arg = db.col_str(2)
|
|
140
|
+
out = row_id.to_s + "|" + job_name + "|" + arg
|
|
141
|
+
end
|
|
142
|
+
db.finalize
|
|
143
|
+
if out.length > 0
|
|
144
|
+
db.prepare("UPDATE tep_jobs SET status = 'running' WHERE id = ?")
|
|
145
|
+
db.bind_int(1, row_id)
|
|
146
|
+
db.step
|
|
147
|
+
db.finalize
|
|
148
|
+
end
|
|
149
|
+
db.close
|
|
150
|
+
out
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Mark the row `done` and store the result string.
|
|
154
|
+
def self.mark_done(db_path, row_id, result)
|
|
155
|
+
db = Tep::SQLite.new
|
|
156
|
+
if !db.open(db_path)
|
|
157
|
+
return 0
|
|
158
|
+
end
|
|
159
|
+
db.prepare("UPDATE tep_jobs SET status = 'done', finished_at = ?, result = ? WHERE id = ?")
|
|
160
|
+
db.bind_int(1, Time.now.to_i)
|
|
161
|
+
db.bind_str(2, result)
|
|
162
|
+
db.bind_int(3, row_id)
|
|
163
|
+
db.step
|
|
164
|
+
db.finalize
|
|
165
|
+
db.close
|
|
166
|
+
1
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Mark the row `failed`. The error message is not stored by this
|
|
170
|
+
# method; the user writes it via their own SQLite calls if they
|
|
171
|
+
# want it persisted.
|
|
172
|
+
def self.mark_failed(db_path, row_id)
|
|
173
|
+
db = Tep::SQLite.new
|
|
174
|
+
if !db.open(db_path)
|
|
175
|
+
return 0
|
|
176
|
+
end
|
|
177
|
+
db.prepare("UPDATE tep_jobs SET status = 'failed', finished_at = ? WHERE id = ?")
|
|
178
|
+
db.bind_int(1, Time.now.to_i)
|
|
179
|
+
db.bind_int(2, row_id)
|
|
180
|
+
db.step
|
|
181
|
+
db.finalize
|
|
182
|
+
db.close
|
|
183
|
+
1
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|