oban 0.5.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.
Files changed (59) hide show
  1. oban/__init__.py +22 -0
  2. oban/__main__.py +12 -0
  3. oban/_backoff.py +87 -0
  4. oban/_config.py +171 -0
  5. oban/_executor.py +188 -0
  6. oban/_extensions.py +16 -0
  7. oban/_leader.py +118 -0
  8. oban/_lifeline.py +77 -0
  9. oban/_notifier.py +324 -0
  10. oban/_producer.py +334 -0
  11. oban/_pruner.py +93 -0
  12. oban/_query.py +409 -0
  13. oban/_recorded.py +34 -0
  14. oban/_refresher.py +88 -0
  15. oban/_scheduler.py +359 -0
  16. oban/_stager.py +115 -0
  17. oban/_worker.py +78 -0
  18. oban/cli.py +436 -0
  19. oban/decorators.py +218 -0
  20. oban/job.py +315 -0
  21. oban/oban.py +1084 -0
  22. oban/py.typed +0 -0
  23. oban/queries/__init__.py +0 -0
  24. oban/queries/ack_job.sql +11 -0
  25. oban/queries/all_jobs.sql +25 -0
  26. oban/queries/cancel_many_jobs.sql +37 -0
  27. oban/queries/cleanup_expired_leaders.sql +4 -0
  28. oban/queries/cleanup_expired_producers.sql +2 -0
  29. oban/queries/delete_many_jobs.sql +5 -0
  30. oban/queries/delete_producer.sql +2 -0
  31. oban/queries/elect_leader.sql +10 -0
  32. oban/queries/fetch_jobs.sql +44 -0
  33. oban/queries/get_job.sql +23 -0
  34. oban/queries/insert_job.sql +28 -0
  35. oban/queries/insert_producer.sql +2 -0
  36. oban/queries/install.sql +113 -0
  37. oban/queries/prune_jobs.sql +18 -0
  38. oban/queries/reelect_leader.sql +12 -0
  39. oban/queries/refresh_producers.sql +3 -0
  40. oban/queries/rescue_jobs.sql +18 -0
  41. oban/queries/reset.sql +5 -0
  42. oban/queries/resign_leader.sql +4 -0
  43. oban/queries/retry_many_jobs.sql +13 -0
  44. oban/queries/stage_jobs.sql +34 -0
  45. oban/queries/uninstall.sql +4 -0
  46. oban/queries/update_job.sql +54 -0
  47. oban/queries/update_producer.sql +3 -0
  48. oban/queries/verify_structure.sql +9 -0
  49. oban/schema.py +115 -0
  50. oban/telemetry/__init__.py +10 -0
  51. oban/telemetry/core.py +170 -0
  52. oban/telemetry/logger.py +147 -0
  53. oban/testing.py +439 -0
  54. oban-0.5.0.dist-info/METADATA +290 -0
  55. oban-0.5.0.dist-info/RECORD +59 -0
  56. oban-0.5.0.dist-info/WHEEL +5 -0
  57. oban-0.5.0.dist-info/entry_points.txt +2 -0
  58. oban-0.5.0.dist-info/licenses/LICENSE.txt +201 -0
  59. oban-0.5.0.dist-info/top_level.txt +1 -0
oban/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,11 @@
1
+ UPDATE oban_jobs
2
+ SET state = %(state)s::oban_job_state,
3
+ cancelled_at = CASE WHEN %(state)s::oban_job_state = 'cancelled' THEN timezone('UTC', now()) ELSE cancelled_at END,
4
+ completed_at = CASE WHEN %(state)s::oban_job_state = 'completed' THEN timezone('UTC', now()) ELSE completed_at END,
5
+ discarded_at = CASE WHEN %(state)s::oban_job_state = 'discarded' THEN timezone('UTC', now()) ELSE discarded_at END,
6
+ scheduled_at = CASE WHEN %(schedule_in)s::int IS NULL THEN scheduled_at ELSE timezone('UTC', now()) + make_interval(secs => %(schedule_in)s::int) END,
7
+ attempt = COALESCE(%(attempt_change)s::int + attempt, attempt),
8
+ errors = CASE WHEN %(error)s::jsonb IS NULL THEN errors ELSE errors || %(error)s::jsonb END,
9
+ meta = CASE WHEN %(meta)s::jsonb IS NULL THEN meta ELSE meta || %(meta)s::jsonb END
10
+ WHERE id = %(id)s
11
+ RETURNING id;
@@ -0,0 +1,25 @@
1
+ SELECT
2
+ id,
3
+ state,
4
+ queue,
5
+ worker,
6
+ attempt,
7
+ max_attempts,
8
+ priority,
9
+ args,
10
+ meta,
11
+ errors,
12
+ tags,
13
+ attempted_by,
14
+ inserted_at,
15
+ attempted_at,
16
+ cancelled_at,
17
+ completed_at,
18
+ discarded_at,
19
+ scheduled_at
20
+ FROM
21
+ oban_jobs
22
+ WHERE
23
+ state = ANY(%(states)s)
24
+ ORDER BY
25
+ id DESC
@@ -0,0 +1,37 @@
1
+ WITH locked_jobs AS (
2
+ SELECT
3
+ id, state
4
+ FROM
5
+ oban_jobs
6
+ WHERE
7
+ id = ANY(%(ids)s)
8
+ AND state IN ('executing', 'available', 'scheduled', 'retryable', 'suspended')
9
+ FOR UPDATE
10
+ ),
11
+ updated_jobs AS (
12
+ UPDATE
13
+ oban_jobs oj
14
+ SET
15
+ state = CASE
16
+ WHEN oj.state = 'executing' THEN oj.state
17
+ ELSE 'cancelled'::oban_job_state
18
+ END,
19
+ cancelled_at = CASE
20
+ WHEN oj.state = 'executing' THEN oj.cancelled_at
21
+ ELSE timezone('UTC', now())
22
+ END,
23
+ meta = CASE
24
+ WHEN oj.state = 'executing' THEN oj.meta
25
+ ELSE oj.meta || jsonb_build_object('cancel_attempted_at', timezone('UTC', now()))
26
+ END
27
+ FROM
28
+ locked_jobs
29
+ WHERE
30
+ oj.id = locked_jobs.id
31
+ RETURNING
32
+ oj.id, locked_jobs.state AS original_state
33
+ )
34
+ SELECT
35
+ id, original_state
36
+ FROM
37
+ updated_jobs
@@ -0,0 +1,4 @@
1
+ DELETE FROM
2
+ oban_leaders
3
+ WHERE
4
+ expires_at < timezone('UTC', now())
@@ -0,0 +1,2 @@
1
+ DELETE FROM oban_producers
2
+ WHERE updated_at < timezone('UTC', now()) - make_interval(secs => %(max_age)s)
@@ -0,0 +1,5 @@
1
+ DELETE FROM
2
+ oban_jobs
3
+ WHERE
4
+ id = ANY(%(ids)s)
5
+ AND state != 'executing'
@@ -0,0 +1,2 @@
1
+ DELETE FROM oban_producers
2
+ WHERE uuid = %(uuid)s
@@ -0,0 +1,10 @@
1
+ INSERT INTO oban_leaders (
2
+ name, node, elected_at, expires_at
3
+ ) VALUES (
4
+ %(name)s,
5
+ %(node)s,
6
+ timezone('UTC', now()),
7
+ timezone('UTC', now()) + interval '%(ttl)s seconds'
8
+ )
9
+ ON CONFLICT (name) DO NOTHING
10
+ RETURNING node
@@ -0,0 +1,44 @@
1
+ WITH locked_jobs AS (
2
+ SELECT
3
+ priority, scheduled_at, id
4
+ FROM
5
+ oban_jobs
6
+ WHERE
7
+ state = 'available'
8
+ AND queue = %(queue)s
9
+ ORDER BY
10
+ priority ASC, scheduled_at ASC, id ASC
11
+ LIMIT
12
+ %(demand)s
13
+ FOR UPDATE SKIP LOCKED
14
+ )
15
+ UPDATE
16
+ oban_jobs oj
17
+ SET
18
+ attempt = oj.attempt + 1,
19
+ attempted_at = timezone('UTC', now()),
20
+ attempted_by = %(attempted_by)s,
21
+ state = 'executing'
22
+ FROM
23
+ locked_jobs
24
+ WHERE
25
+ oj.id = locked_jobs.id
26
+ RETURNING
27
+ oj.id,
28
+ oj.state,
29
+ oj.queue,
30
+ oj.worker,
31
+ oj.attempt,
32
+ oj.max_attempts,
33
+ oj.priority,
34
+ oj.args,
35
+ oj.meta,
36
+ oj.errors,
37
+ oj.tags,
38
+ oj.attempted_by,
39
+ oj.inserted_at,
40
+ oj.attempted_at,
41
+ oj.cancelled_at,
42
+ oj.completed_at,
43
+ oj.discarded_at,
44
+ oj.scheduled_at
@@ -0,0 +1,23 @@
1
+ SELECT
2
+ id,
3
+ state,
4
+ queue,
5
+ worker,
6
+ attempt,
7
+ max_attempts,
8
+ priority,
9
+ args,
10
+ meta,
11
+ errors,
12
+ tags,
13
+ attempted_by,
14
+ inserted_at,
15
+ attempted_at,
16
+ cancelled_at,
17
+ completed_at,
18
+ discarded_at,
19
+ scheduled_at
20
+ FROM
21
+ oban_jobs
22
+ WHERE
23
+ id = %s
@@ -0,0 +1,28 @@
1
+ INSERT INTO oban_jobs(
2
+ args,
3
+ inserted_at,
4
+ max_attempts,
5
+ meta,
6
+ priority,
7
+ queue,
8
+ scheduled_at,
9
+ state,
10
+ tags,
11
+ worker
12
+ ) VALUES (
13
+ %(args)s,
14
+ coalesce(%(inserted_at)s, timezone('UTC', now())),
15
+ %(max_attempts)s,
16
+ %(meta)s,
17
+ %(priority)s,
18
+ %(queue)s,
19
+ coalesce(%(scheduled_at)s, timezone('UTC', now())),
20
+ CASE
21
+ WHEN %(state)s = 'available' AND %(scheduled_at)s IS NOT NULL
22
+ THEN 'scheduled'::oban_job_state
23
+ ELSE %(state)s::oban_job_state
24
+ END,
25
+ %(tags)s,
26
+ %(worker)s
27
+ )
28
+ RETURNING id, inserted_at, queue, scheduled_at, state;
@@ -0,0 +1,2 @@
1
+ INSERT INTO oban_producers (uuid, name, node, queue, meta)
2
+ VALUES (%(uuid)s, %(name)s, %(node)s, %(queue)s, %(meta)s)
@@ -0,0 +1,113 @@
1
+ -- Types
2
+
3
+ DO $$
4
+ BEGIN
5
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'oban_job_state') THEN
6
+ CREATE TYPE oban_job_state AS ENUM (
7
+ 'available',
8
+ 'scheduled',
9
+ 'suspended',
10
+ 'executing',
11
+ 'retryable',
12
+ 'completed',
13
+ 'discarded',
14
+ 'cancelled'
15
+ );
16
+ END IF;
17
+ END
18
+ $$;
19
+
20
+ -- Tables
21
+
22
+ CREATE TABLE IF NOT EXISTS oban_jobs (
23
+ id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
24
+ state oban_job_state NOT NULL DEFAULT 'available',
25
+ queue text NOT NULL DEFAULT 'default',
26
+ worker text NOT NULL,
27
+ attempt smallint NOT NULL DEFAULT 0,
28
+ max_attempts smallint NOT NULL DEFAULT 20,
29
+ priority smallint NOT NULL DEFAULT 0,
30
+ args jsonb NOT NULL DEFAULT '{}',
31
+ meta jsonb NOT NULL DEFAULT '{}',
32
+ tags jsonb NOT NULL DEFAULT '[]',
33
+ errors jsonb NOT NULL DEFAULT '[]',
34
+ attempted_by text[] NOT NULL DEFAULT ARRAY[]::TEXT[],
35
+ inserted_at timestamp WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC', now()),
36
+ scheduled_at timestamp WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC', now()),
37
+ attempted_at timestamp WITHOUT TIME ZONE,
38
+ cancelled_at timestamp WITHOUT TIME ZONE,
39
+ completed_at timestamp WITHOUT TIME ZONE,
40
+ discarded_at timestamp WITHOUT TIME ZONE,
41
+
42
+ CONSTRAINT attempt_range CHECK (attempt >= 0 AND attempt <= max_attempts),
43
+ CONSTRAINT queue_length CHECK (char_length(queue) > 0),
44
+ CONSTRAINT worker_length CHECK (char_length(worker) > 0),
45
+ CONSTRAINT positive_max_attempts CHECK (max_attempts > 0),
46
+ CONSTRAINT non_negative_priority CHECK (priority >= 0)
47
+ );
48
+
49
+ CREATE UNLOGGED TABLE IF NOT EXISTS oban_leaders (
50
+ name text PRIMARY KEY DEFAULT 'oban',
51
+ node text NOT NULL,
52
+ elected_at timestamp WITHOUT TIME ZONE NOT NULL,
53
+ expires_at timestamp WITHOUT TIME ZONE NOT NULL
54
+ );
55
+
56
+ CREATE UNLOGGED TABLE IF NOT EXISTS oban_producers (
57
+ uuid uuid PRIMARY KEY,
58
+ name text NOT NULL DEFAULT 'oban',
59
+ node text NOT NULL,
60
+ queue text NOT NULL,
61
+ meta jsonb NOT NULL DEFAULT '{}',
62
+ started_at timestamp WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC', now()),
63
+ updated_at timestamp WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC', now())
64
+ );
65
+
66
+ -- Indexes
67
+
68
+ CREATE INDEX IF NOT EXISTS oban_jobs_state_queue_priority_scheduled_at_id_index
69
+ ON oban_jobs (state, queue, priority, scheduled_at, id)
70
+ WITH (fillfactor = 90);
71
+
72
+ CREATE INDEX IF NOT EXISTS oban_jobs_staging_index
73
+ ON oban_jobs (scheduled_at, id)
74
+ WHERE state IN ('scheduled', 'retryable');
75
+
76
+ CREATE INDEX IF NOT EXISTS oban_jobs_completed_at_index
77
+ ON oban_jobs (completed_at)
78
+ WHERE state = 'completed';
79
+
80
+ CREATE INDEX IF NOT EXISTS oban_jobs_cancelled_at_index
81
+ ON oban_jobs (cancelled_at)
82
+ WHERE state = 'cancelled';
83
+
84
+ CREATE INDEX IF NOT EXISTS oban_jobs_discarded_at_index
85
+ ON oban_jobs (discarded_at)
86
+ WHERE state = 'discarded';
87
+
88
+ -- Autovacuum
89
+
90
+ ALTER TABLE oban_jobs SET (
91
+ -- Vacuum earlier on large tables
92
+ autovacuum_vacuum_scale_factor = 0.02,
93
+ autovacuum_vacuum_threshold = 50,
94
+
95
+ -- Keep stats fresh for the planner
96
+ autovacuum_analyze_scale_factor = 0.02,
97
+ autovacuum_analyze_threshold = 100,
98
+
99
+ -- Make autovacuum push harder with little/no sleeping
100
+ autovacuum_vacuum_cost_limit = 2000,
101
+ autovacuum_vacuum_cost_delay = 1,
102
+
103
+ -- Handle insert-heavy spikes (PG13+)
104
+ autovacuum_vacuum_insert_scale_factor = 0.02,
105
+ autovacuum_vacuum_insert_threshold = 1000,
106
+
107
+ -- Leave headroom on pages for locality and fewer page splits
108
+ fillfactor = 85
109
+ );
110
+
111
+ -- Version
112
+
113
+ COMMENT ON TABLE oban_jobs IS '1';
@@ -0,0 +1,18 @@
1
+ WITH jobs_to_delete AS (
2
+ SELECT
3
+ id
4
+ FROM
5
+ oban_jobs
6
+ WHERE
7
+ (state = 'completed' AND completed_at <= timezone('UTC', now()) - make_interval(secs => %(max_age)s)) OR
8
+ (state = 'cancelled' AND cancelled_at <= timezone('UTC', now()) - make_interval(secs => %(max_age)s)) OR
9
+ (state = 'discarded' AND discarded_at <= timezone('UTC', now()) - make_interval(secs => %(max_age)s))
10
+ ORDER BY
11
+ id ASC
12
+ LIMIT
13
+ %(limit)s
14
+ )
15
+ DELETE FROM
16
+ oban_jobs
17
+ WHERE
18
+ id IN (SELECT id FROM jobs_to_delete)
@@ -0,0 +1,12 @@
1
+ INSERT INTO oban_leaders (name, node, elected_at, expires_at)
2
+ VALUES (
3
+ %(name)s,
4
+ %(node)s,
5
+ timezone('UTC', now()),
6
+ timezone('UTC', now()) + interval '%(ttl)s seconds'
7
+ )
8
+ ON CONFLICT (name) DO UPDATE SET
9
+ expires_at = EXCLUDED.expires_at
10
+ WHERE
11
+ oban_leaders.node = EXCLUDED.node
12
+ RETURNING node
@@ -0,0 +1,3 @@
1
+ UPDATE oban_producers
2
+ SET updated_at = timezone('UTC', now())
3
+ WHERE uuid = ANY(%(uuids)s)
@@ -0,0 +1,18 @@
1
+ UPDATE
2
+ oban_jobs
3
+ SET
4
+ state = CASE
5
+ WHEN attempt >= max_attempts THEN 'discarded'::oban_job_state
6
+ ELSE 'available'::oban_job_state
7
+ END,
8
+ discarded_at = CASE
9
+ WHEN attempt >= max_attempts THEN timezone('UTC', now())
10
+ ELSE discarded_at
11
+ END,
12
+ meta = CASE
13
+ WHEN attempt >= max_attempts THEN meta
14
+ ELSE meta || jsonb_build_object('rescued', coalesce((meta->>'rescued')::int, 0) + 1)
15
+ END
16
+ WHERE
17
+ state = 'executing'
18
+ AND attempted_at < timezone('UTC', now()) - make_interval(secs => %(rescue_after)s)
oban/queries/reset.sql ADDED
@@ -0,0 +1,5 @@
1
+ TRUNCATE TABLE
2
+ oban_jobs,
3
+ oban_leaders,
4
+ oban_producers
5
+ RESTART IDENTITY CASCADE
@@ -0,0 +1,4 @@
1
+ DELETE FROM
2
+ oban_leaders
3
+ WHERE
4
+ name = %(name)s AND node = %(node)s
@@ -0,0 +1,13 @@
1
+ UPDATE
2
+ oban_jobs
3
+ SET
4
+ state = 'available'::oban_job_state,
5
+ max_attempts = GREATEST(max_attempts, attempt + 1),
6
+ scheduled_at = timezone('UTC', now()),
7
+ completed_at = NULL,
8
+ cancelled_at = NULL,
9
+ discarded_at = NULL,
10
+ meta = jsonb_set(meta, '{uniq_bmp}', '[]'::jsonb)
11
+ WHERE
12
+ id = ANY(%(ids)s)
13
+ AND state NOT IN ('available', 'executing')
@@ -0,0 +1,34 @@
1
+ WITH locked_jobs AS (
2
+ SELECT
3
+ id
4
+ FROM
5
+ oban_jobs
6
+ WHERE
7
+ state = ANY('{scheduled,retryable}')
8
+ AND scheduled_at <= coalesce(%(before)s, timezone('UTC', now()))
9
+ ORDER BY
10
+ scheduled_at ASC, id ASC
11
+ LIMIT
12
+ %(limit)s
13
+ FOR UPDATE SKIP LOCKED
14
+ ),
15
+ updated_jobs AS (
16
+ UPDATE
17
+ oban_jobs
18
+ SET
19
+ state = 'available'::oban_job_state
20
+ FROM
21
+ locked_jobs
22
+ WHERE
23
+ oban_jobs.id = locked_jobs.id
24
+ )
25
+ SELECT DISTINCT
26
+ q.queue
27
+ FROM
28
+ unnest(%(queues)s::text[]) AS q(queue)
29
+ WHERE
30
+ EXISTS (
31
+ SELECT 1
32
+ FROM oban_jobs
33
+ WHERE state = 'available' AND queue = q.queue
34
+ )
@@ -0,0 +1,4 @@
1
+ DROP TABLE IF EXISTS oban_producers CASCADE;
2
+ DROP TABLE IF EXISTS oban_leaders CASCADE;
3
+ DROP TABLE IF EXISTS oban_jobs CASCADE;
4
+ DROP TYPE IF EXISTS oban_job_state CASCADE;
@@ -0,0 +1,54 @@
1
+ WITH raw_job_data AS (
2
+ SELECT
3
+ unnest(%(ids)s::bigint[]) AS id,
4
+ unnest(%(args)s::jsonb[]) AS args,
5
+ unnest(%(max_attempts)s::smallint[]) AS max_attempts,
6
+ unnest(%(meta)s::jsonb[]) AS meta,
7
+ unnest(%(priority)s::smallint[]) AS priority,
8
+ unnest(%(queue)s::text[]) AS queue,
9
+ unnest(%(scheduled_at)s::timestamptz[]) AS scheduled_at,
10
+ unnest(%(tags)s::jsonb[]) AS tags,
11
+ unnest(%(worker)s::text[]) AS worker
12
+ ),
13
+ locked_jobs AS (
14
+ SELECT
15
+ id
16
+ FROM
17
+ oban_jobs
18
+ WHERE
19
+ id = ANY(%(ids)s)
20
+ FOR UPDATE
21
+ )
22
+ UPDATE
23
+ oban_jobs oj
24
+ SET
25
+ args = rjd.args,
26
+ max_attempts = rjd.max_attempts,
27
+ meta = rjd.meta,
28
+ priority = rjd.priority,
29
+ queue = rjd.queue,
30
+ scheduled_at = rjd.scheduled_at,
31
+ state = CASE
32
+ WHEN oj.state = 'available' AND rjd.scheduled_at > timezone('UTC', now())
33
+ THEN 'scheduled'::oban_job_state
34
+ WHEN oj.state = 'scheduled' AND rjd.scheduled_at <= timezone('UTC', now())
35
+ THEN 'available'::oban_job_state
36
+ ELSE oj.state
37
+ END,
38
+ tags = rjd.tags,
39
+ worker = rjd.worker
40
+ FROM
41
+ raw_job_data rjd
42
+ INNER JOIN locked_jobs lj ON rjd.id = lj.id
43
+ WHERE
44
+ oj.id = rjd.id
45
+ RETURNING
46
+ oj.args,
47
+ oj.max_attempts,
48
+ oj.meta,
49
+ oj.priority,
50
+ oj.queue,
51
+ oj.scheduled_at,
52
+ oj.state,
53
+ oj.tags,
54
+ oj.worker;
@@ -0,0 +1,3 @@
1
+ UPDATE oban_producers
2
+ SET meta = meta || %(meta)s
3
+ WHERE uuid = %(uuid)s
@@ -0,0 +1,9 @@
1
+ SELECT
2
+ table_name
3
+ FROM
4
+ information_schema.tables
5
+ WHERE
6
+ table_schema = %(prefix)s
7
+ AND table_name = ANY('{oban_jobs,oban_leaders,oban_producers}')
8
+ ORDER BY
9
+ table_name
oban/schema.py ADDED
@@ -0,0 +1,115 @@
1
+ """Database schema installation for Oban."""
2
+
3
+ from typing import Any
4
+
5
+ from ._query import Query
6
+
7
+ INITIAL_VERSION = 1
8
+ CURRENT_VERSION = 1
9
+
10
+
11
+ def install_sql(prefix: str = "public") -> str:
12
+ """Get the SQL for installing Oban.
13
+
14
+ Returns the raw SQL statements for creating Oban types, tables, and indexes.
15
+ This is intended for integration with migration frameworks like Django or Alembic.
16
+
17
+ Args:
18
+ prefix: PostgreSQL schema where Oban tables will be located (default: "public")
19
+
20
+ Returns:
21
+ SQL string for schema installation
22
+
23
+ Example (Alembic):
24
+ >>> from alembic import op
25
+ >>> from oban.schema import install_sql
26
+ >>>
27
+ >>> def upgrade():
28
+ ... op.execute(install_sql())
29
+
30
+ Example (Django):
31
+ >>> from django.db import migrations
32
+ >>> from oban.schema import install_sql
33
+ >>>
34
+ >>> class Migration(migrations.Migration):
35
+ ... operations = [
36
+ ... migrations.RunSQL(install_sql()),
37
+ ... ]
38
+ """
39
+ return Query._load_file("install.sql", prefix)
40
+
41
+
42
+ def uninstall_sql(prefix: str = "public") -> str:
43
+ """Get the SQL for uninstalling Oban.
44
+
45
+ Returns the raw SQL statements for dropping Oban tables and types.
46
+ Useful for integration with migration frameworks like Alembic or Django.
47
+
48
+ Args:
49
+ prefix: PostgreSQL schema where Oban tables are located (default: "public")
50
+
51
+ Returns:
52
+ SQL string for schema uninstallation
53
+
54
+ Example (Alembic):
55
+ >>> from alembic import op
56
+ >>> from oban.schema import uninstall_sql
57
+ >>>
58
+ >>> def downgrade():
59
+ ... op.execute(uninstall_sql())
60
+
61
+ Example (Django):
62
+ >>> from django.db import migrations
63
+ >>> from oban.schema import uninstall_sql
64
+ >>>
65
+ >>> class Migration(migrations.Migration):
66
+ ... operations = [
67
+ ... migrations.RunSQL(uninstall_sql()),
68
+ ... ]
69
+ """
70
+ return Query._load_file("uninstall.sql", prefix)
71
+
72
+
73
+ async def install(pool: Any, prefix: str = "public") -> None:
74
+ """Install Oban in the specified database.
75
+
76
+ Creates all necessary types, tables, and indexes for Oban to function. The
77
+ installation is wrapped in a DDL transaction to ensure the operation is
78
+ atomic.
79
+
80
+ Args:
81
+ pool: A database connection pool (e.g., AsyncConnectionPool)
82
+ prefix: PostgreSQL schema where Oban tables will be located (default: "public")
83
+
84
+ Example:
85
+ >>> from psycopg_pool import AsyncConnectionPool
86
+ >>> from oban.schema import install
87
+ >>>
88
+ >>> pool = AsyncConnectionPool(conninfo=DATABASE_URL, open=False)
89
+ >>> await pool.open()
90
+ >>> await install(pool)
91
+ """
92
+ async with pool.connection() as conn:
93
+ await conn.execute(install_sql(prefix))
94
+
95
+
96
+ async def uninstall(pool: Any, prefix: str = "public") -> None:
97
+ """Uninstall Oban from the specified database.
98
+
99
+ Drops all Oban tables and types. The uninstallation is wrapped in a DDL
100
+ transaction to ensure the operation is atomic.
101
+
102
+ Args:
103
+ pool: A database connection pool (e.g., AsyncConnectionPool)
104
+ prefix: PostgreSQL schema where Oban tables are located (default: "public")
105
+
106
+ Example:
107
+ >>> from psycopg_pool import AsyncConnectionPool
108
+ >>> from oban.schema import uninstall
109
+ >>>
110
+ >>> pool = AsyncConnectionPool(conninfo=DATABASE_URL, open=False)
111
+ >>> await pool.open()
112
+ >>> await uninstall(pool)
113
+ """
114
+ async with pool.connection() as conn:
115
+ await conn.execute(uninstall_sql(prefix))
@@ -0,0 +1,10 @@
1
+ """
2
+ Lightweight telemetry tooling for agnostic instrumentation.
3
+
4
+ Provides event emission and handler attachment for instrumentation,
5
+ similar to Elixir's `:telemetry` library, but tailored to Oban's needs.
6
+ """
7
+
8
+ from .core import Collector, attach, detach, execute, span
9
+
10
+ __all__ = ["Collector", "attach", "detach", "execute", "span"]