python-pq 0.2.3__tar.gz → 0.3.1__tar.gz
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.
- {python_pq-0.2.3 → python_pq-0.3.1}/PKG-INFO +3 -2
- {python_pq-0.2.3 → python_pq-0.3.1}/README.md +1 -1
- {python_pq-0.2.3 → python_pq-0.3.1}/pyproject.toml +4 -3
- {python_pq-0.2.3 → python_pq-0.3.1}/src/pq/client.py +78 -3
- python_pq-0.3.1/src/pq/migrations/README +1 -0
- python_pq-0.3.1/src/pq/migrations/__init__.py +1 -0
- python_pq-0.3.1/src/pq/migrations/env.py +85 -0
- python_pq-0.3.1/src/pq/migrations/script.py.mako +28 -0
- python_pq-0.3.1/src/pq/migrations/versions/20260109T055839Z_476683af098d_initial_schema.py +97 -0
- python_pq-0.3.1/src/pq/migrations/versions/20260109T063747Z_2483bec70083_add_client_id.py +54 -0
- python_pq-0.3.1/src/pq/migrations/versions/__init__.py +1 -0
- {python_pq-0.2.3 → python_pq-0.3.1}/src/pq/models.py +6 -0
- {python_pq-0.2.3 → python_pq-0.3.1}/src/pq/__init__.py +0 -0
- {python_pq-0.2.3 → python_pq-0.3.1}/src/pq/config.py +0 -0
- {python_pq-0.2.3 → python_pq-0.3.1}/src/pq/logging.py +0 -0
- {python_pq-0.2.3 → python_pq-0.3.1}/src/pq/priority.py +0 -0
- {python_pq-0.2.3 → python_pq-0.3.1}/src/pq/registry.py +0 -0
- {python_pq-0.2.3 → python_pq-0.3.1}/src/pq/serialization.py +0 -0
- {python_pq-0.2.3 → python_pq-0.3.1}/src/pq/worker.py +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-pq
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Postgres-backed job queue for Python
|
|
5
5
|
Author: ricwo
|
|
6
6
|
Author-email: ricwo <r@cogram.com>
|
|
7
|
+
Requires-Dist: alembic>=1.17.2
|
|
7
8
|
Requires-Dist: click>=8.3.1
|
|
8
9
|
Requires-Dist: croniter>=6.0.0
|
|
9
10
|
Requires-Dist: dill>=0.4.0
|
|
@@ -45,7 +46,7 @@ Requires PostgreSQL and Python 3.13+.
|
|
|
45
46
|
from pq import PQ
|
|
46
47
|
|
|
47
48
|
pq = PQ("postgresql://localhost/mydb")
|
|
48
|
-
pq.
|
|
49
|
+
pq.run_db_migrations() # Creates/updates tables
|
|
49
50
|
|
|
50
51
|
def send_email(to: str, subject: str) -> None:
|
|
51
52
|
print(f"Sending to {to}: {subject}")
|
|
@@ -25,7 +25,7 @@ Requires PostgreSQL and Python 3.13+.
|
|
|
25
25
|
from pq import PQ
|
|
26
26
|
|
|
27
27
|
pq = PQ("postgresql://localhost/mydb")
|
|
28
|
-
pq.
|
|
28
|
+
pq.run_db_migrations() # Creates/updates tables
|
|
29
29
|
|
|
30
30
|
def send_email(to: str, subject: str) -> None:
|
|
31
31
|
print(f"Sending to {to}: {subject}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-pq"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Postgres-backed job queue for Python"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -8,6 +8,7 @@ authors = [
|
|
|
8
8
|
]
|
|
9
9
|
requires-python = ">=3.13,<3.15"
|
|
10
10
|
dependencies = [
|
|
11
|
+
"alembic>=1.17.2",
|
|
11
12
|
"click>=8.3.1",
|
|
12
13
|
"croniter>=6.0.0",
|
|
13
14
|
"dill>=0.4.0",
|
|
@@ -38,8 +39,8 @@ module-name = "pq"
|
|
|
38
39
|
dev = [
|
|
39
40
|
"pre-commit>=4.5.1",
|
|
40
41
|
"pytest>=9.0.2",
|
|
41
|
-
"ruff>=0.14.
|
|
42
|
-
"ty>=0.0.
|
|
42
|
+
"ruff>=0.14.11",
|
|
43
|
+
"ty>=0.0.10",
|
|
43
44
|
]
|
|
44
45
|
|
|
45
46
|
[tool.pytest.ini_options]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""PQ client - main interface for task queue."""
|
|
2
2
|
|
|
3
|
+
import importlib.resources
|
|
3
4
|
from collections.abc import Callable, Set
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
from datetime import UTC, datetime, timedelta
|
|
@@ -65,12 +66,41 @@ class PQ:
|
|
|
65
66
|
"""Exit context manager and close connections."""
|
|
66
67
|
self.close()
|
|
67
68
|
|
|
69
|
+
def run_db_migrations(self) -> None:
|
|
70
|
+
"""Run database migrations to latest version.
|
|
71
|
+
|
|
72
|
+
Call this once at application startup before using the queue.
|
|
73
|
+
Uses Alembic to apply any pending migrations. Safe to call
|
|
74
|
+
multiple times - only pending migrations are applied.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
pq = PQ("postgresql://localhost/mydb")
|
|
78
|
+
pq.run_db_migrations()
|
|
79
|
+
"""
|
|
80
|
+
# Lazy import to avoid fork issues on macOS
|
|
81
|
+
from alembic import command
|
|
82
|
+
from alembic.config import Config
|
|
83
|
+
|
|
84
|
+
# Get migrations directory from within the installed package
|
|
85
|
+
migrations_pkg = importlib.resources.files("pq.migrations")
|
|
86
|
+
migrations_dir = str(migrations_pkg)
|
|
87
|
+
|
|
88
|
+
alembic_cfg = Config()
|
|
89
|
+
alembic_cfg.set_main_option("script_location", migrations_dir)
|
|
90
|
+
alembic_cfg.set_main_option("sqlalchemy.url", str(self._engine.url))
|
|
91
|
+
command.upgrade(alembic_cfg, "head")
|
|
92
|
+
|
|
68
93
|
def create_tables(self) -> None:
|
|
69
|
-
"""Create all tables (for testing).
|
|
94
|
+
"""Create all tables directly (for testing only).
|
|
95
|
+
|
|
96
|
+
For production, use run_db_migrations() instead. This method
|
|
97
|
+
bypasses Alembic and creates tables directly via SQLAlchemy,
|
|
98
|
+
which doesn't track schema versions.
|
|
99
|
+
"""
|
|
70
100
|
Base.metadata.create_all(self._engine)
|
|
71
101
|
|
|
72
102
|
def drop_tables(self) -> None:
|
|
73
|
-
"""Drop all tables (for testing)."""
|
|
103
|
+
"""Drop all tables (for testing only)."""
|
|
74
104
|
Base.metadata.drop_all(self._engine)
|
|
75
105
|
|
|
76
106
|
def clear_all(self) -> None:
|
|
@@ -85,6 +115,7 @@ class PQ:
|
|
|
85
115
|
*args: Any,
|
|
86
116
|
run_at: datetime | None = None,
|
|
87
117
|
priority: Priority = Priority.NORMAL,
|
|
118
|
+
client_id: str | None = None,
|
|
88
119
|
**kwargs: Any,
|
|
89
120
|
) -> int:
|
|
90
121
|
"""Enqueue a one-off task.
|
|
@@ -94,6 +125,7 @@ class PQ:
|
|
|
94
125
|
*args: Positional arguments to pass to the handler.
|
|
95
126
|
run_at: When to run the task. Defaults to now.
|
|
96
127
|
priority: Task priority. Higher = higher priority. Defaults to NORMAL.
|
|
128
|
+
client_id: Optional client-provided identifier. Must be unique if provided.
|
|
97
129
|
**kwargs: Keyword arguments to pass to the handler.
|
|
98
130
|
|
|
99
131
|
Returns:
|
|
@@ -101,6 +133,7 @@ class PQ:
|
|
|
101
133
|
|
|
102
134
|
Raises:
|
|
103
135
|
ValueError: If task is a lambda, closure, or cannot be imported.
|
|
136
|
+
IntegrityError: If client_id already exists.
|
|
104
137
|
"""
|
|
105
138
|
name = get_function_path(task)
|
|
106
139
|
payload = serialize(args, kwargs)
|
|
@@ -108,7 +141,13 @@ class PQ:
|
|
|
108
141
|
if run_at is None:
|
|
109
142
|
run_at = datetime.now(UTC)
|
|
110
143
|
|
|
111
|
-
task_obj = Task(
|
|
144
|
+
task_obj = Task(
|
|
145
|
+
name=name,
|
|
146
|
+
payload=payload,
|
|
147
|
+
run_at=run_at,
|
|
148
|
+
priority=priority,
|
|
149
|
+
client_id=client_id,
|
|
150
|
+
)
|
|
112
151
|
|
|
113
152
|
with self.session() as session:
|
|
114
153
|
session.add(task_obj)
|
|
@@ -122,6 +161,7 @@ class PQ:
|
|
|
122
161
|
run_every: timedelta | None = None,
|
|
123
162
|
cron: str | croniter | None = None,
|
|
124
163
|
priority: Priority = Priority.NORMAL,
|
|
164
|
+
client_id: str | None = None,
|
|
125
165
|
**kwargs: Any,
|
|
126
166
|
) -> int:
|
|
127
167
|
"""Schedule a periodic task.
|
|
@@ -135,6 +175,7 @@ class PQ:
|
|
|
135
175
|
run_every: Interval between executions (e.g., timedelta(hours=1)).
|
|
136
176
|
cron: Cron expression string (e.g., "0 9 * * 1") or croniter object.
|
|
137
177
|
priority: Task priority. Higher = higher priority. Defaults to NORMAL.
|
|
178
|
+
client_id: Optional client-provided identifier. Must be unique if provided.
|
|
138
179
|
**kwargs: Keyword arguments to pass to the handler.
|
|
139
180
|
|
|
140
181
|
Returns:
|
|
@@ -144,6 +185,7 @@ class PQ:
|
|
|
144
185
|
ValueError: If neither run_every nor cron is provided, or if both are.
|
|
145
186
|
ValueError: If cron expression is invalid.
|
|
146
187
|
ValueError: If task is a lambda, closure, or cannot be imported.
|
|
188
|
+
IntegrityError: If client_id already exists.
|
|
147
189
|
"""
|
|
148
190
|
if run_every is None and cron is None:
|
|
149
191
|
raise ValueError("Either run_every or cron must be provided")
|
|
@@ -185,6 +227,7 @@ class PQ:
|
|
|
185
227
|
run_every=run_every,
|
|
186
228
|
cron=cron_expr,
|
|
187
229
|
next_run=next_run,
|
|
230
|
+
client_id=client_id,
|
|
188
231
|
)
|
|
189
232
|
.on_conflict_do_update(
|
|
190
233
|
index_elements=["name"],
|
|
@@ -261,6 +304,38 @@ class PQ:
|
|
|
261
304
|
session.expunge(task)
|
|
262
305
|
return task
|
|
263
306
|
|
|
307
|
+
def get_task_by_client_id(self, client_id: str) -> Task | None:
|
|
308
|
+
"""Get a task by client_id.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
client_id: Client-provided identifier.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Task object or None if not found.
|
|
315
|
+
"""
|
|
316
|
+
with self.session() as session:
|
|
317
|
+
stmt = select(Task).where(Task.client_id == client_id)
|
|
318
|
+
task = session.execute(stmt).scalar_one_or_none()
|
|
319
|
+
if task:
|
|
320
|
+
session.expunge(task)
|
|
321
|
+
return task
|
|
322
|
+
|
|
323
|
+
def get_periodic_by_client_id(self, client_id: str) -> Periodic | None:
|
|
324
|
+
"""Get a periodic task by client_id.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
client_id: Client-provided identifier.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Periodic object or None if not found.
|
|
331
|
+
"""
|
|
332
|
+
with self.session() as session:
|
|
333
|
+
stmt = select(Periodic).where(Periodic.client_id == client_id)
|
|
334
|
+
periodic = session.execute(stmt).scalar_one_or_none()
|
|
335
|
+
if periodic:
|
|
336
|
+
session.expunge(periodic)
|
|
337
|
+
return periodic
|
|
338
|
+
|
|
264
339
|
def list_failed(self, limit: int = 100) -> list[Task]:
|
|
265
340
|
"""List failed tasks.
|
|
266
341
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Generic single-database configuration.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Alembic migrations for pq database schema."""
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from logging.config import fileConfig
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import engine_from_config, pool
|
|
4
|
+
|
|
5
|
+
from alembic import context
|
|
6
|
+
|
|
7
|
+
from pq.models import Base
|
|
8
|
+
|
|
9
|
+
# this is the Alembic Config object, which provides
|
|
10
|
+
# access to the values within the .ini file in use.
|
|
11
|
+
config = context.config
|
|
12
|
+
|
|
13
|
+
# Interpret the config file for Python logging.
|
|
14
|
+
# This line sets up loggers basically.
|
|
15
|
+
if config.config_file_name is not None:
|
|
16
|
+
fileConfig(config.config_file_name)
|
|
17
|
+
|
|
18
|
+
# pq models metadata for autogenerate support
|
|
19
|
+
target_metadata = Base.metadata
|
|
20
|
+
|
|
21
|
+
# Custom version table name to match pq_ naming convention
|
|
22
|
+
VERSION_TABLE = "pq_schema_version"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_migrations_offline() -> None:
|
|
26
|
+
"""Run migrations in 'offline' mode.
|
|
27
|
+
|
|
28
|
+
This configures the context with just a URL
|
|
29
|
+
and not an Engine, though an Engine is acceptable
|
|
30
|
+
here as well. By skipping the Engine creation
|
|
31
|
+
we don't even need a DBAPI to be available.
|
|
32
|
+
|
|
33
|
+
Calls to context.execute() here emit the given string to the
|
|
34
|
+
script output.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
38
|
+
context.configure(
|
|
39
|
+
url=url,
|
|
40
|
+
target_metadata=target_metadata,
|
|
41
|
+
literal_binds=True,
|
|
42
|
+
dialect_opts={"paramstyle": "named"},
|
|
43
|
+
version_table=VERSION_TABLE,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
with context.begin_transaction():
|
|
47
|
+
context.run_migrations()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_migrations_online() -> None:
|
|
51
|
+
"""Run migrations in 'online' mode.
|
|
52
|
+
|
|
53
|
+
In this scenario we need to create an Engine
|
|
54
|
+
and associate a connection with the context.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
# Support both ini file config and programmatic config
|
|
58
|
+
config_section = config.get_section(config.config_ini_section, {})
|
|
59
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
60
|
+
|
|
61
|
+
if url and not config_section.get("sqlalchemy.url"):
|
|
62
|
+
# URL was set programmatically, add it to config section
|
|
63
|
+
config_section["sqlalchemy.url"] = url
|
|
64
|
+
|
|
65
|
+
connectable = engine_from_config(
|
|
66
|
+
config_section,
|
|
67
|
+
prefix="sqlalchemy.",
|
|
68
|
+
poolclass=pool.NullPool,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
with connectable.connect() as connection:
|
|
72
|
+
context.configure(
|
|
73
|
+
connection=connection,
|
|
74
|
+
target_metadata=target_metadata,
|
|
75
|
+
version_table=VERSION_TABLE,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
with context.begin_transaction():
|
|
79
|
+
context.run_migrations()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if context.is_offline_mode():
|
|
83
|
+
run_migrations_offline()
|
|
84
|
+
else:
|
|
85
|
+
run_migrations_online()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
${imports if imports else ""}
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = ${repr(up_revision)}
|
|
16
|
+
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
"""Upgrade schema."""
|
|
23
|
+
${upgrades if upgrades else "pass"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
"""Downgrade schema."""
|
|
28
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""initial schema
|
|
2
|
+
|
|
3
|
+
Revision ID: 476683af098d
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2026-01-09 05:58:39.589866 Z
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
from sqlalchemy.dialects import postgresql
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "476683af098d"
|
|
17
|
+
down_revision: Union[str, Sequence[str], None] = None
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
"""Upgrade schema."""
|
|
24
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
25
|
+
op.create_table(
|
|
26
|
+
"pq_periodic",
|
|
27
|
+
sa.Column("id", sa.BigInteger(), sa.Identity(always=False), nullable=False),
|
|
28
|
+
sa.Column("name", sa.String(length=255), nullable=False),
|
|
29
|
+
sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
30
|
+
sa.Column("priority", sa.SmallInteger(), nullable=False),
|
|
31
|
+
sa.Column("run_every", sa.Interval(), nullable=True),
|
|
32
|
+
sa.Column("cron", sa.String(length=100), nullable=True),
|
|
33
|
+
sa.Column("next_run", sa.DateTime(timezone=True), nullable=False),
|
|
34
|
+
sa.Column("last_run", sa.DateTime(timezone=True), nullable=True),
|
|
35
|
+
sa.Column(
|
|
36
|
+
"created_at",
|
|
37
|
+
sa.DateTime(timezone=True),
|
|
38
|
+
server_default=sa.text("now()"),
|
|
39
|
+
nullable=False,
|
|
40
|
+
),
|
|
41
|
+
sa.PrimaryKeyConstraint("id"),
|
|
42
|
+
sa.UniqueConstraint("name"),
|
|
43
|
+
)
|
|
44
|
+
op.create_index(
|
|
45
|
+
"ix_pq_periodic_priority_next_run",
|
|
46
|
+
"pq_periodic",
|
|
47
|
+
["priority", "next_run"],
|
|
48
|
+
unique=False,
|
|
49
|
+
)
|
|
50
|
+
op.create_table(
|
|
51
|
+
"pq_tasks",
|
|
52
|
+
sa.Column("id", sa.BigInteger(), sa.Identity(always=False), nullable=False),
|
|
53
|
+
sa.Column("name", sa.String(length=255), nullable=False),
|
|
54
|
+
sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
55
|
+
sa.Column("priority", sa.SmallInteger(), nullable=False),
|
|
56
|
+
sa.Column(
|
|
57
|
+
"status",
|
|
58
|
+
sa.Enum(
|
|
59
|
+
"PENDING",
|
|
60
|
+
"RUNNING",
|
|
61
|
+
"COMPLETED",
|
|
62
|
+
"FAILED",
|
|
63
|
+
name="task_status",
|
|
64
|
+
create_constraint=True,
|
|
65
|
+
),
|
|
66
|
+
nullable=False,
|
|
67
|
+
),
|
|
68
|
+
sa.Column("run_at", sa.DateTime(timezone=True), nullable=False),
|
|
69
|
+
sa.Column(
|
|
70
|
+
"created_at",
|
|
71
|
+
sa.DateTime(timezone=True),
|
|
72
|
+
server_default=sa.text("now()"),
|
|
73
|
+
nullable=False,
|
|
74
|
+
),
|
|
75
|
+
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
|
76
|
+
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
|
77
|
+
sa.Column("error", sa.Text(), nullable=True),
|
|
78
|
+
sa.Column("attempts", sa.Integer(), nullable=False),
|
|
79
|
+
sa.PrimaryKeyConstraint("id"),
|
|
80
|
+
)
|
|
81
|
+
op.create_index(
|
|
82
|
+
"ix_pq_tasks_status_priority_run_at",
|
|
83
|
+
"pq_tasks",
|
|
84
|
+
["status", "priority", "run_at"],
|
|
85
|
+
unique=False,
|
|
86
|
+
)
|
|
87
|
+
# ### end Alembic commands ###
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def downgrade() -> None:
|
|
91
|
+
"""Downgrade schema."""
|
|
92
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
93
|
+
op.drop_index("ix_pq_tasks_status_priority_run_at", table_name="pq_tasks")
|
|
94
|
+
op.drop_table("pq_tasks")
|
|
95
|
+
op.drop_index("ix_pq_periodic_priority_next_run", table_name="pq_periodic")
|
|
96
|
+
op.drop_table("pq_periodic")
|
|
97
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""add client_id
|
|
2
|
+
|
|
3
|
+
Revision ID: 2483bec70083
|
|
4
|
+
Revises: 476683af098d
|
|
5
|
+
Create Date: 2026-01-09 06:37:47 Z
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "2483bec70083"
|
|
17
|
+
down_revision: Union[str, Sequence[str], None] = "476683af098d"
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
"""Add client_id column to pq_tasks and pq_periodic tables."""
|
|
24
|
+
# Add client_id column to pq_tasks
|
|
25
|
+
op.add_column(
|
|
26
|
+
"pq_tasks",
|
|
27
|
+
sa.Column("client_id", sa.String(length=255), nullable=True),
|
|
28
|
+
)
|
|
29
|
+
op.create_index(
|
|
30
|
+
"ix_pq_tasks_client_id",
|
|
31
|
+
"pq_tasks",
|
|
32
|
+
["client_id"],
|
|
33
|
+
unique=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Add client_id column to pq_periodic
|
|
37
|
+
op.add_column(
|
|
38
|
+
"pq_periodic",
|
|
39
|
+
sa.Column("client_id", sa.String(length=255), nullable=True),
|
|
40
|
+
)
|
|
41
|
+
op.create_index(
|
|
42
|
+
"ix_pq_periodic_client_id",
|
|
43
|
+
"pq_periodic",
|
|
44
|
+
["client_id"],
|
|
45
|
+
unique=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def downgrade() -> None:
|
|
50
|
+
"""Remove client_id column from pq_tasks and pq_periodic tables."""
|
|
51
|
+
op.drop_index("ix_pq_periodic_client_id", table_name="pq_periodic")
|
|
52
|
+
op.drop_column("pq_periodic", "client_id")
|
|
53
|
+
op.drop_index("ix_pq_tasks_client_id", table_name="pq_tasks")
|
|
54
|
+
op.drop_column("pq_tasks", "client_id")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Migration versions."""
|
|
@@ -45,6 +45,9 @@ class Task(Base):
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
id: Mapped[int] = mapped_column(BigInteger, Identity(), primary_key=True)
|
|
48
|
+
client_id: Mapped[str | None] = mapped_column(
|
|
49
|
+
String(255), nullable=True, unique=True, index=True
|
|
50
|
+
)
|
|
48
51
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
49
52
|
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
|
50
53
|
priority: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
|
|
@@ -76,6 +79,9 @@ class Periodic(Base):
|
|
|
76
79
|
)
|
|
77
80
|
|
|
78
81
|
id: Mapped[int] = mapped_column(BigInteger, Identity(), primary_key=True)
|
|
82
|
+
client_id: Mapped[str | None] = mapped_column(
|
|
83
|
+
String(255), nullable=True, unique=True, index=True
|
|
84
|
+
)
|
|
79
85
|
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
|
80
86
|
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
|
81
87
|
priority: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|