PgQueuer 0.0.1__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.

Potentially problematic release.


This version of PgQueuer might be problematic. Click here for more details.

PgQueuer/__init__.py ADDED
File without changes
PgQueuer/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ import asyncio
2
+
3
+ from PgQueuer import cli
4
+
5
+ if __name__ == "__main__":
6
+ asyncio.run(cli.main())
PgQueuer/_version.py ADDED
@@ -0,0 +1,17 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+
7
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
8
+ else:
9
+ VERSION_TUPLE = object
10
+
11
+ version: str
12
+ __version__: str
13
+ __version_tuple__: VERSION_TUPLE
14
+ version_tuple: VERSION_TUPLE
15
+
16
+ __version__ = version = "0.1.dev1+gac06244.d20240420"
17
+ __version_tuple__ = version_tuple = (0, 1, "dev1", "gac06244.d20240420")
PgQueuer/cli.py ADDED
@@ -0,0 +1,130 @@
1
+ import argparse
2
+ import os
3
+
4
+ import asyncpg
5
+
6
+ from PgQueuer import queries
7
+
8
+
9
+ def cliparser() -> argparse.Namespace:
10
+ common_arguments = argparse.ArgumentParser(
11
+ add_help=False,
12
+ prog="PgQueuer",
13
+ )
14
+
15
+ common_arguments.add_argument(
16
+ "--prefix",
17
+ default="",
18
+ help=(
19
+ "All PgQueuer tables/functions/etc... will start with this prefix. "
20
+ "(If set, addinal config is required.)"
21
+ ),
22
+ )
23
+
24
+ common_arguments.add_argument(
25
+ "--pg-dsn",
26
+ help=(
27
+ "Connection string in the libpq URI format, including host, port, user, "
28
+ "database, password, passfile, and SSL options. Must be properly quoted; "
29
+ "IPv6 addresses must be in brackets. "
30
+ "Example: postgres://user:pass@host:port/database. Defaults to PGDSN "
31
+ "environment variable if set."
32
+ ),
33
+ default=os.environ.get("PGDSN"),
34
+ )
35
+
36
+ common_arguments.add_argument(
37
+ "--pg-host",
38
+ help=(
39
+ "Database host address, which can be an IP or domain name. "
40
+ "Defaults to PGHOST environment variable if set."
41
+ ),
42
+ default=os.environ.get("PGHOST"),
43
+ )
44
+
45
+ common_arguments.add_argument(
46
+ "--pg-port",
47
+ help=(
48
+ "Port number for the server host Defaults to PGPORT environment variable "
49
+ "or 5432 if not set."
50
+ ),
51
+ default=os.environ.get("PGPORT", "5432"),
52
+ )
53
+
54
+ common_arguments.add_argument(
55
+ "--pg-user",
56
+ help=(
57
+ "Database role for authentication. Defaults to PGUSER environment "
58
+ "variable if set."
59
+ ),
60
+ default=os.environ.get("PGUSER"),
61
+ )
62
+
63
+ common_arguments.add_argument(
64
+ "--pg-database",
65
+ help=(
66
+ "Name of the database to connect to. Defaults to PGDATABASE environment "
67
+ "variable if set."
68
+ ),
69
+ default=os.environ.get("PGDATABASE"),
70
+ )
71
+
72
+ common_arguments.add_argument(
73
+ "--pg-password",
74
+ help=(
75
+ "Password for authentication. Defaults to PGPASSWORD "
76
+ "environment variable if set"
77
+ ),
78
+ default=os.environ.get("PGPASSWORD"),
79
+ )
80
+
81
+ parser = argparse.ArgumentParser(
82
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
83
+ prog="pgcachewatch",
84
+ )
85
+
86
+ subparsers = parser.add_subparsers(
87
+ dest="command",
88
+ required=True,
89
+ )
90
+
91
+ subparsers.add_parser(
92
+ "install",
93
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
94
+ parents=[common_arguments],
95
+ )
96
+
97
+ subparsers.add_parser(
98
+ "uninstall",
99
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
100
+ parents=[common_arguments],
101
+ )
102
+
103
+ return parser.parse_args()
104
+
105
+
106
+ async def main() -> None:
107
+ parsed = cliparser()
108
+
109
+ if (
110
+ "PGQUEUER_PREFIX" not in os.environ
111
+ and isinstance(prefix := parsed.prefix, str)
112
+ and prefix
113
+ ):
114
+ os.environ["PGQUEUER_PREFIX"] = prefix
115
+
116
+ async with asyncpg.create_pool(
117
+ parsed.pg_dsn,
118
+ database=parsed.pg_database,
119
+ password=parsed.pg_password,
120
+ port=parsed.pg_port,
121
+ user=parsed.pg_user,
122
+ host=parsed.pg_host,
123
+ min_size=0,
124
+ max_size=1,
125
+ ) as pool:
126
+ match parsed.command:
127
+ case "install":
128
+ await queries.InstallUninstallQueries(pool=pool).install()
129
+ case "uninstall":
130
+ await queries.InstallUninstallQueries(pool=pool).uninstall()
PgQueuer/logconfig.py ADDED
@@ -0,0 +1,7 @@
1
+ import logging
2
+ import os
3
+ from typing import Final
4
+
5
+ logging.basicConfig()
6
+ logger: Final = logging.getLogger("pgqueuer")
7
+ logger.setLevel(level=os.environ.get("LOGLEVEL", "INFO").upper())
PgQueuer/models.py ADDED
@@ -0,0 +1,32 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import AwareDatetime, BaseModel, RootModel
4
+
5
+ STATUS = Literal[
6
+ "queued",
7
+ "picked",
8
+ ]
9
+ STATUS_LOG = Literal[
10
+ "exception",
11
+ "successful",
12
+ ]
13
+
14
+
15
+ class Job(BaseModel):
16
+ """
17
+ Represents a job with attributes such as ID, priority,
18
+ creation time, status, entrypoint, and optional payload.
19
+ """
20
+
21
+ id: int
22
+ priority: int
23
+ created: AwareDatetime
24
+ status: STATUS
25
+ entrypoint: str
26
+ payload: bytes | None
27
+
28
+
29
+ class Jobs(RootModel[list[Job]]):
30
+ """
31
+ A collection model that encapsulates a list of Job instances.
32
+ """
PgQueuer/py.typed ADDED
File without changes
PgQueuer/qm.py ADDED
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import dataclasses
5
+ from typing import Awaitable, Callable, TypeAlias, TypeVar
6
+
7
+ import asyncpg
8
+ from pgcachewatch.listeners import PGEventQueue
9
+ from pgcachewatch.models import PGChannel
10
+
11
+ from .logconfig import logger
12
+ from .models import Job
13
+ from .queries import PgQueuerLogQueries, PgQueuerQueries
14
+ from .tm import TaskManager
15
+
16
+ EntrypointFn: TypeAlias = Callable[[bytes | None], Awaitable[None]]
17
+ T = TypeVar("T", bound=EntrypointFn)
18
+
19
+
20
+ @dataclasses.dataclass
21
+ class QueueManager:
22
+ """
23
+ Manages job queues and dispatches jobs to registered entry points,
24
+ handling database connections and events.
25
+ """
26
+
27
+ pool: asyncpg.Pool
28
+ q: PgQueuerQueries = dataclasses.field(init=False)
29
+ ql: PgQueuerLogQueries = dataclasses.field(init=False)
30
+
31
+ channel: PGChannel = dataclasses.field(
32
+ default=PGChannel("ch_pgqueuer"),
33
+ init=False,
34
+ )
35
+ alive: bool = dataclasses.field(
36
+ init=False,
37
+ default=True,
38
+ )
39
+ # Should registry be a weakref?
40
+ registry: dict[str, EntrypointFn] = dataclasses.field(
41
+ init=False,
42
+ default_factory=dict,
43
+ )
44
+ tm: TaskManager = dataclasses.field(
45
+ init=False,
46
+ default_factory=TaskManager,
47
+ )
48
+
49
+ def __post_init__(self) -> None:
50
+ """
51
+ Initializes database query handlers and validates pool size upon
52
+ instance creation.
53
+ """
54
+ if self.pool.get_min_size() < 1:
55
+ raise ValueError("... min size must be gt 1.")
56
+ self.q = PgQueuerQueries(self.pool)
57
+ self.ql = PgQueuerLogQueries(self.pool)
58
+
59
+ def entrypoint(self, name: str) -> Callable[[T], T]:
60
+ """
61
+ Decorator to register a function as an entrypoint for
62
+ handling specific job types.
63
+ """
64
+
65
+ def register(func: T) -> T:
66
+ if name in self.registry:
67
+ raise RuntimeError(f"{name} already in registry, must be unique.")
68
+ self.registry[name] = func
69
+ return func
70
+
71
+ return register
72
+
73
+ async def run(self) -> None:
74
+ """
75
+ Starts the event listener and continuously dispatches jobs to
76
+ registered entry points until stopped.
77
+ """
78
+ async with self.pool.acquire() as conn:
79
+ listener = PGEventQueue()
80
+ await listener.connect(conn, self.channel)
81
+
82
+ while self.alive:
83
+ while (jobs := await self.q.dequeue()).root:
84
+ for job in jobs.root:
85
+ self._dispatch(job)
86
+ await listener.get()
87
+
88
+ await asyncio.gather(*self.tm.tasks)
89
+
90
+ def _dispatch(self, job: Job) -> None:
91
+ """
92
+ Internal method to asynchronously handle job dispatch,
93
+ including exception logging and job status updates.
94
+ """
95
+
96
+ async def runit() -> None:
97
+ logger.debug(
98
+ "Dispatching entrypoint/id: %s/%s",
99
+ job.entrypoint,
100
+ job.id,
101
+ )
102
+ try:
103
+ await self.registry[job.entrypoint](job.payload)
104
+ except Exception:
105
+ logger.exception(
106
+ "Exception while processing entrypoint/id: %s/%s",
107
+ job.entrypoint,
108
+ job.id,
109
+ )
110
+ await self.ql.move_job_log(job, "exception")
111
+ else:
112
+ logger.debug(
113
+ "Dispatching entrypoint/id: %s/%s - successful",
114
+ job.entrypoint,
115
+ job.id,
116
+ )
117
+ await self.ql.move_job_log(job, "successful")
118
+
119
+ self.tm.add(asyncio.create_task(runit()))
PgQueuer/queries.py ADDED
@@ -0,0 +1,281 @@
1
+ import dataclasses
2
+ import os
3
+ from typing import Final
4
+
5
+ import asyncpg
6
+
7
+ from . import models
8
+
9
+
10
+ def add_prefix(string: str) -> str:
11
+ """
12
+ Appends a predefined prefix from environment variables to
13
+ the given string, typically used for database object names.
14
+ """
15
+ return f"{os.environ.get('PGQUEUER_PREFIX', '')}{string}"
16
+
17
+
18
+ @dataclasses.dataclass
19
+ class DBSettings:
20
+ """
21
+ Stores database settings such as table names and SQL
22
+ function names, dynamically appending prefixes from
23
+ environment variables.
24
+ """
25
+
26
+ channel: Final[str] = dataclasses.field(
27
+ default_factory=lambda: add_prefix("ch_pgqueuer"),
28
+ kw_only=True,
29
+ )
30
+ function: Final[str] = dataclasses.field(
31
+ default_factory=lambda: add_prefix("fn_pgqueuer_changed"),
32
+ kw_only=True,
33
+ )
34
+ log_table: Final[str] = dataclasses.field(
35
+ default_factory=lambda: add_prefix("pgqueuer_log"),
36
+ kw_only=True,
37
+ )
38
+ log_table_status_type: Final[str] = dataclasses.field(
39
+ default_factory=lambda: add_prefix("pgqueuer_status_log"),
40
+ kw_only=True,
41
+ )
42
+ queue_status_type: Final[str] = dataclasses.field(
43
+ default_factory=lambda: add_prefix("pgqueuer_status"),
44
+ kw_only=True,
45
+ )
46
+ queue_table: Final[str] = dataclasses.field(
47
+ default_factory=lambda: add_prefix("pgqueuer"),
48
+ kw_only=True,
49
+ )
50
+ trigger: Final[str] = dataclasses.field(
51
+ default_factory=lambda: add_prefix("tg_pgqueuer_changed"),
52
+ kw_only=True,
53
+ )
54
+
55
+
56
+ @dataclasses.dataclass
57
+ class InstallUninstallQueries:
58
+ """
59
+ Provides methods to install (create) and uninstall (drop)
60
+ database schemas and objects like tables, types, and triggers
61
+ related to the pgqueuer system.
62
+ """
63
+
64
+ pool: asyncpg.Pool
65
+ settings: DBSettings = dataclasses.field(default_factory=DBSettings)
66
+
67
+ async def install(self) -> None:
68
+ """
69
+ Creates necessary database structures such as enums,
70
+ tables, and triggers for job queuing and logging.
71
+ """
72
+ await self.pool.execute(
73
+ f"""
74
+ CREATE TYPE {self.settings.queue_status_type} AS ENUM ('queued', 'picked');
75
+ CREATE TABLE {self.settings.queue_table} (
76
+ id SERIAL PRIMARY KEY,
77
+ priority INT NOT NULL,
78
+ created TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
79
+ status {self.settings.queue_status_type} NOT NULL,
80
+ entrypoint TEXT NOT NULL,
81
+ payload BYTEA
82
+ );
83
+ CREATE UNIQUE INDEX ON {self.settings.queue_table} (priority, id) WHERE status != 'picked';
84
+
85
+ CREATE TYPE {self.settings.log_table_status_type} AS ENUM ('exception', 'successful');
86
+ CREATE TABLE {self.settings.log_table} (
87
+ id SERIAL PRIMARY KEY,
88
+ priority INT NOT NULL,
89
+ created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
90
+ duration INTERVAL NOT NULL,
91
+ status {self.settings.log_table_status_type} NOT NULL,
92
+ entrypoint TEXT NOT NULL
93
+ );
94
+
95
+
96
+ CREATE FUNCTION {self.settings.function}() RETURNS TRIGGER AS $$
97
+ BEGIN
98
+ PERFORM pg_notify(
99
+ '{self.settings.channel}',
100
+ json_build_object(
101
+ 'channel', '{self.settings.channel}',
102
+ 'operation', lower(TG_OP),
103
+ 'sent_at', NOW(),
104
+ 'table', TG_TABLE_NAME
105
+ )::text);
106
+ RETURN NEW;
107
+ END;
108
+ $$ LANGUAGE plpgsql;
109
+
110
+ CREATE TRIGGER {self.settings.trigger}
111
+ AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON {self.settings.queue_table}
112
+ EXECUTE FUNCTION {self.settings.function}();
113
+ """ # noqa: E501
114
+ )
115
+
116
+ async def uninstall(self) -> None:
117
+ """
118
+ Drops all database structures related to job queuing
119
+ and logging that were created by the install method.
120
+ """
121
+ await self.pool.execute(
122
+ f"""
123
+ DROP TRIGGER {self.settings.trigger};
124
+ DROP FUNCTION {self.settings.function};
125
+ DROP TABLE {self.settings.queue_table};
126
+ DROP TABLE {self.settings.log_table};
127
+ DROP TYPE {self.settings.queue_status_type};
128
+ DROP TYPE {self.settings.log_table_status_type};
129
+ """
130
+ )
131
+
132
+
133
+ @dataclasses.dataclass
134
+ class PgQueuerQueries:
135
+ """
136
+ Handles operations related to job queuing such as
137
+ enqueueing, dequeueing, and querying the size of the queue.
138
+ """
139
+
140
+ pool: asyncpg.Pool
141
+ settings: DBSettings = dataclasses.field(default_factory=DBSettings)
142
+
143
+ async def dequeue(self) -> models.Jobs:
144
+ """
145
+ Retrieves and updates the next 'queued' job to 'picked'
146
+ status, ensuring no two jobs with the same entrypoint
147
+ are picked simultaneously.
148
+ """
149
+ query = f"""
150
+ WITH next_job AS (
151
+ SELECT p1.id
152
+ FROM {self.settings.queue_table} p1
153
+ WHERE p1.status = 'queued' AND NOT EXISTS (
154
+ SELECT FROM
155
+ {self.settings.queue_table} p2
156
+ WHERE p1.entrypoint = p2.entrypoint AND p2.status = 'picked')
157
+ ORDER BY id
158
+ FOR UPDATE SKIP LOCKED
159
+ LIMIT 1
160
+ )
161
+ UPDATE {self.settings.queue_table}
162
+ SET status = 'picked'
163
+ WHERE id = ANY(SELECT id FROM next_job)
164
+ RETURNING *;
165
+ """
166
+
167
+ return models.Jobs.model_validate(map(dict, await self.pool.fetch(query)))
168
+
169
+ async def enqueue(
170
+ self,
171
+ entrypoint: str,
172
+ payload: bytes | None,
173
+ priority: int = 0,
174
+ ) -> None:
175
+ """
176
+ Inserts a new job into the queue with the specified
177
+ entrypoint, payload, and priority, marking it as 'queued'.
178
+ """
179
+ await self.pool.execute(
180
+ f"""
181
+ INSERT INTO {self.settings.queue_table} (priority, status, entrypoint, payload)
182
+ VALUES ($1, 'queued', $2, $3)""",
183
+ priority,
184
+ entrypoint,
185
+ payload,
186
+ )
187
+
188
+ async def clear(self, entrypoint: str | list[str] | None = None) -> None:
189
+ """
190
+ Clears jobs from the queue, optionally filtering by entrypoint if specified.
191
+ """
192
+ if entrypoint:
193
+ await self.pool.execute(
194
+ f"DELETE FROM {self.settings.queue_table} WHERE entrypoint = ANY($1)",
195
+ [entrypoint] if isinstance(entrypoint, str) else entrypoint,
196
+ )
197
+ else:
198
+ await self.pool.execute(f"TRUNCATE {self.settings.queue_table}")
199
+
200
+ async def qsize(self) -> dict[tuple[str, int], int]:
201
+ """
202
+ Returns the number of jobs in the queue grouped by entrypoint and priority.
203
+ """
204
+ return {
205
+ (row["entrypoint"], row["priority"]): row["cnt"]
206
+ for row in await self.pool.fetch(f"""
207
+ SELECT
208
+ count(*) AS cnt,
209
+ priority,
210
+ entrypoint
211
+ FROM
212
+ {self.settings.queue_table}
213
+ GROUP BY priority, entrypoint
214
+ """)
215
+ }
216
+
217
+
218
+ @dataclasses.dataclass
219
+ class PgQueuerLogQueries:
220
+ """
221
+ Manages operations related to job logging,
222
+ including moving jobs to a log table, clearing logs,
223
+ and querying the size of log entries.
224
+ """
225
+
226
+ pool: asyncpg.Pool
227
+ settings: DBSettings = dataclasses.field(default_factory=DBSettings)
228
+
229
+ async def move_job_log(self, job: models.Job, status: models.STATUS_LOG) -> None:
230
+ """
231
+ Moves a completed or failed job from the queue table to the log
232
+ table, recording its final status and duration.
233
+ """
234
+ await self.pool.execute(
235
+ f"""
236
+ WITH moved_row AS (
237
+ DELETE FROM {self.settings.queue_table}
238
+ WHERE id = $1
239
+ RETURNING id, priority, created, entrypoint
240
+ )
241
+
242
+ INSERT INTO {self.settings.log_table} (
243
+ id, priority, created, duration, status, entrypoint
244
+ )
245
+ SELECT id, priority, created, NOW() - created, $2, entrypoint
246
+ FROM moved_row;
247
+ """,
248
+ job.id,
249
+ status,
250
+ )
251
+
252
+ async def clear(self, entrypoint: str | list[str] | None = None) -> None:
253
+ """
254
+ Clears entries from the job log table, optionally filtering
255
+ by entrypoint if specified.
256
+ """
257
+ if entrypoint:
258
+ await self.pool.execute(
259
+ f"DELETE FROM {self.settings.log_table} WHERE entrypoint = ANY($1)",
260
+ [entrypoint] if isinstance(entrypoint, str) else entrypoint,
261
+ )
262
+ else:
263
+ await self.pool.execute(f"TRUNCATE {self.settings.log_table}")
264
+
265
+ async def qsize(self) -> dict[tuple[str, str, int], int]:
266
+ """
267
+ Returns the number of log entries grouped by status, entrypoint, and priority.
268
+ """
269
+ return {
270
+ (row["status"], row["entrypoint"], row["priority"]): row["cnt"]
271
+ for row in await self.pool.fetch(f"""
272
+ SELECT
273
+ count(*) AS cnt,
274
+ status,
275
+ priority,
276
+ entrypoint
277
+ FROM
278
+ {self.settings.log_table}
279
+ GROUP BY status, priority, entrypoint
280
+ """)
281
+ }
PgQueuer/tm.py ADDED
@@ -0,0 +1,23 @@
1
+ import asyncio
2
+ import dataclasses
3
+
4
+
5
+ @dataclasses.dataclass
6
+ class TaskManager:
7
+ """
8
+ Manages a collection of asyncio Tasks, keeping track of active
9
+ tasks and removing them once they are complete.
10
+ """
11
+
12
+ tasks: set[asyncio.Task] = dataclasses.field(
13
+ default_factory=set,
14
+ init=False,
15
+ )
16
+
17
+ def add(self, task: asyncio.Task) -> None:
18
+ """
19
+ Adds an asyncio Task to the manager and registers a
20
+ callback to automatically remove the task when it's done.
21
+ """
22
+ self.tasks.add(task)
23
+ task.add_done_callback(self.tasks.remove)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 janbjorge
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.1
2
+ Name: PgQueuer
3
+ Version: 0.0.1
4
+ Summary: PgQueuer is a Python library leveraging PostgreSQL for efficient job queuing.
5
+ Author: janbjorge
6
+ License: MIT License
7
+ Project-URL: Documentation, https://github.com/janbjorge/PgQueuer/
8
+ Project-URL: Homepage, https://github.com/janbjorge/PgQueuer/
9
+ Project-URL: Issues, https://github.com/janbjorge/PgQueuer/issues
10
+ Project-URL: Repository, https://github.com/janbjorge/PgQueuer/
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Natural Language :: English
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python
22
+ Classifier: Topic :: Database
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Utilities
25
+ Classifier: Topic :: System :: Distributed Computing
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: asyncpg >=0.27.0
30
+ Requires-Dist: pgcachewatch >=0.4
31
+ Requires-Dist: pydantic >=2.0.0
32
+ Provides-Extra: dev
33
+ Requires-Dist: asyncpg-stubs ; extra == 'dev'
34
+ Requires-Dist: mypy-extensions ; extra == 'dev'
35
+ Requires-Dist: mypy ; extra == 'dev'
36
+ Requires-Dist: pytest-asyncio ; extra == 'dev'
37
+ Requires-Dist: pytest ; extra == 'dev'
38
+ Requires-Dist: ruff ; extra == 'dev'
39
+ Provides-Extra: docs
40
+ Requires-Dist: myst-parser ; extra == 'docs'
41
+ Requires-Dist: sphinx ; extra == 'docs'
42
+ Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
43
+
44
+ ## PgQueuer
45
+
46
+ PgQueuer is a minimalist, high-performance job queue library for Python, leveraging the robustness of PostgreSQL. Designed for simplicity and efficiency, PgQueuer uses PostgreSQL's native features like transactions, row locking, and LISTEN/NOTIFY to manage job queues effortlessly.
47
+
48
+ ### Features
49
+
50
+ - **Simple Integration**: Easy to integrate with existing Python applications using PostgreSQL.
51
+ - **Efficient Concurrency Handling**: Utilizes PostgreSQL's `FOR UPDATE SKIP LOCKED` for reliable and concurrent job processing.
52
+ - **Real-time Notifications**: Leverages `LISTEN` and `NOTIFY` for real-time updates on job status changes.
53
+ - **Scalable**: Built to efficiently scale with your application's needs.
54
+
55
+ ### Getting Started
56
+
57
+ #### Installation
58
+
59
+ To install PgQueuer, simply run the following command:
60
+
61
+ ```bash
62
+ pip install PgQueuer
63
+ ```
64
+
65
+ This command installs PgQueuer along with its dependencies, setting up everything you need to start integrating it into your application.
66
+
67
+ #### Database Configuration
68
+
69
+ Configure your database connection settings by setting the appropriate environment variables for your PostgreSQL credentials:
70
+
71
+ ```bash
72
+ export PGHOST='localhost'
73
+ export PGDATABASE='your_database'
74
+ export PGUSER='your_username'
75
+ export PGPASSWORD='your_password'
76
+ ```
77
+
78
+ #### CLI Usage
79
+
80
+ PgQueuer provides a command-line interface for easy management of installation and uninstallation. Ensure you have configured your environment variables or use the appropriate flags to specify your database credentials.
81
+
82
+ - **Installing PgQueuer Database Components**:
83
+ ```bash
84
+ python -m PgQueuer install
85
+ ```
86
+
87
+ - **Uninstalling PgQueuer Database Components**:
88
+ ```bash
89
+ python -m uninstall
90
+ ```
91
+
92
+ The CLI supports several flags to customize the connection settings. Use `--help` to see all available options.
93
+
94
+ ### Use-Case: Processing Incoming Data Messages
95
+
96
+ In this scenario, the system is designed to manage a queue of incoming data messages that need to be processed. Each message is encapsulated as a job, and these jobs are then processed by a designated function linked to an entrypoint. This is typical in systems where large volumes of data are collected continuously, such as telemetry data from IoT devices, logs from web servers, or transaction data in financial systems.
97
+
98
+ #### Function Description:
99
+ The `fetch` function, tied to the `fetch` entrypoint, is responsible for processing each message. It simulates a processing task by printing the content of each message. This function could be adapted to perform more complex operations such as parsing, analyzing, storing, or forwarding the data as required by the application.
100
+
101
+ ```python
102
+ import asyncio
103
+ import time
104
+
105
+ import asyncpg
106
+
107
+ from PgQueuer import qm, queries
108
+
109
+
110
+ async def main() -> None:
111
+ # Set up a connection pool with a minimum of 2 connections.
112
+ pool = await asyncpg.create_pool(min_size=2)
113
+ # Initialize the QueueManager with the connection pool and query handler.
114
+ app = qm.QueueManager(
115
+ pool,
116
+ queries.PgQueuerQueries(pool),
117
+ queries.PgQueuerLogQueries(pool),
118
+ )
119
+
120
+ # Number of messages to simulate.
121
+ N = 10_000
122
+
123
+ # Enqueue messages.
124
+ for n in range(N):
125
+ await app.q.enqueue("fetch", f"this is from me: {n}".encode())
126
+
127
+ # Define a processing function for each message.
128
+ @c.entrypoint("fetch")
129
+ async def process_message(context: bytes) -> None:
130
+ # Simulate parsing the message.
131
+ message = context.decode()
132
+ # Print the message to simulate processing.
133
+ print(f"Processed message: {message}")
134
+
135
+ await app.run()
136
+
137
+
138
+ if __name__ == "__main__":
139
+ asyncio.run(main())
140
+ ```
@@ -0,0 +1,16 @@
1
+ _version.py,sha256=pMnmqZnpVmaqR5nqHztNWzbbtb1oy5bPN_v7uhOH8K8,411
2
+ PgQueuer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ PgQueuer/__main__.py,sha256=Dww9T08Xg8kdRFEvHpXpTA2trY7G-lhafwZf1IUqm6c,97
4
+ PgQueuer/_version.py,sha256=YonpGHgj9UzI33RW5RhfA9spxQnBoSxMf0rWyg16ezg,461
5
+ PgQueuer/cli.py,sha256=kri8GI_Zynn93VP7eRcTx2Kzsxze5SWIQoFIT2mkkbc,3529
6
+ PgQueuer/logconfig.py,sha256=nCfcp663CduoBSqcWjZ6nWRkDJDBC-c98erj0hGCvZc,185
7
+ PgQueuer/models.py,sha256=e4r_3RU1soAD-HgFSn-sWeBLqI50cSi4h-4lo-VUetY,594
8
+ PgQueuer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ PgQueuer/qm.py,sha256=hys7igBkWDQbW2q41q_9Ye_jVJ0-z4OmyvKc31CHzFc,3659
10
+ PgQueuer/queries.py,sha256=Qovzi_zJXeCEFRMvAuAByEkGJ7CALendcepdcYWT5E4,8967
11
+ PgQueuer/tm.py,sha256=t6qU_-pv94D5XeDXtME5O85JRzS3BnaLTZqg57YED0k,596
12
+ PgQueuer-0.0.1.dist-info/LICENSE,sha256=0lCGUd5Pke2Up1Kqn1kRnHcwLlBz60E8xvR7WL4k_kE,1066
13
+ PgQueuer-0.0.1.dist-info/METADATA,sha256=EgHpTY4L0oJuzKIBM-c4ZrP4LbOdgfJSgYXY7SdM-4E,5376
14
+ PgQueuer-0.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
+ PgQueuer-0.0.1.dist-info/top_level.txt,sha256=7mMxIeRADcMrOwkYdxmOC5S5aydHM7PRnauwqqHHCtk,18
16
+ PgQueuer-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ PgQueuer
2
+ _version
_version.py ADDED
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.0.1'
16
+ __version_tuple__ = version_tuple = (0, 0, 1)