datanommer.models 1.2.0__py3-none-any.whl → 1.4.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.
@@ -32,6 +32,7 @@ from sqlalchemy import (
32
32
  event,
33
33
  ForeignKey,
34
34
  func,
35
+ Index,
35
36
  Integer,
36
37
  not_,
37
38
  or_,
@@ -158,6 +159,7 @@ def add(message):
158
159
  timestamp=sent_at,
159
160
  msg=message.body,
160
161
  headers=headers,
162
+ agent_name=getattr(message, "agent_name", None),
161
163
  users=usernames,
162
164
  packages=packages,
163
165
  )
@@ -168,7 +170,7 @@ def add(message):
168
170
  # https://docs.sqlalchemy.org/en/14/core/custom_types.html#marshal-json-strings
169
171
 
170
172
 
171
- class JSONEncodedDict(TypeDecorator):
173
+ class _JSONEncodedDict(TypeDecorator):
172
174
  """Represents an immutable structure as a json-encoded string."""
173
175
 
174
176
  impl = UnicodeText
@@ -213,7 +215,15 @@ packages_assoc_table = Table(
213
215
 
214
216
  class Message(DeclarativeBase):
215
217
  __tablename__ = "messages"
216
- __table_args__ = (UniqueConstraint("msg_id", "timestamp"),)
218
+ __table_args__ = (
219
+ UniqueConstraint("msg_id", "timestamp"),
220
+ Index(
221
+ "ix_messages_headers",
222
+ "headers",
223
+ postgresql_using="gin",
224
+ postgresql_ops={"headers": "jsonb_path_ops"},
225
+ ),
226
+ )
217
227
 
218
228
  id = Column(Integer, primary_key=True, autoincrement=True)
219
229
  msg_id = Column(Unicode, nullable=True, default=None, index=True)
@@ -223,11 +233,11 @@ class Message(DeclarativeBase):
223
233
  certificate = Column(UnicodeText)
224
234
  signature = Column(UnicodeText)
225
235
  category = Column(Unicode, nullable=False, index=True)
226
- username = Column(Unicode)
236
+ agent_name = Column(Unicode, index=True)
227
237
  crypto = Column(UnicodeText)
228
238
  source_name = Column(Unicode, default="datanommer")
229
239
  source_version = Column(Unicode, default=lambda context: __version__)
230
- msg = Column(JSONEncodedDict, nullable=False)
240
+ msg = Column(_JSONEncodedDict, nullable=False)
231
241
  headers = Column(postgresql.JSONB(none_as_null=True))
232
242
  users = relationship(
233
243
  "User",
@@ -326,7 +336,8 @@ class Message(DeclarativeBase):
326
336
  timestamp=self.timestamp,
327
337
  certificate=self.certificate,
328
338
  signature=self.signature,
329
- username=self.username,
339
+ agent_name=self.agent_name,
340
+ username=self.agent_name, # DEPRECATED
330
341
  crypto=self.crypto,
331
342
  msg=self.msg,
332
343
  headers=self.headers,
@@ -358,14 +369,21 @@ class Message(DeclarativeBase):
358
369
  )
359
370
  return self.as_dict(request)
360
371
 
372
+ @property
373
+ def username(self):
374
+ warn(
375
+ "The username attribute has been renamed to agent_name, and will be removed "
376
+ "in the next major version",
377
+ DeprecationWarning,
378
+ stacklevel=2,
379
+ )
380
+ return self.agent_name
381
+
361
382
  @classmethod
362
- def grep(
383
+ def make_query(
363
384
  cls,
364
385
  start=None,
365
386
  end=None,
366
- page=1,
367
- rows_per_page=100,
368
- order="asc",
369
387
  msg_id=None,
370
388
  users=None,
371
389
  not_users=None,
@@ -375,8 +393,9 @@ class Message(DeclarativeBase):
375
393
  not_categories=None,
376
394
  topics=None,
377
395
  not_topics=None,
396
+ agents=None,
397
+ not_agents=None,
378
398
  contains=None,
379
- defer=False,
380
399
  ):
381
400
  """Flexible query interface for messages.
382
401
 
@@ -405,10 +424,6 @@ class Message(DeclarativeBase):
405
424
  (user == 'ralph') AND
406
425
  NOT (category == 'bodhi' OR category == 'wiki')
407
426
 
408
- ----
409
-
410
- If the `defer` argument evaluates to True, the query won't actually
411
- be executed, but a SQLAlchemy query object returned instead.
412
427
  """
413
428
 
414
429
  users = users or []
@@ -419,6 +434,8 @@ class Message(DeclarativeBase):
419
434
  not_cats = not_categories or []
420
435
  topics = topics or []
421
436
  not_topics = not_topics or []
437
+ agents = agents or []
438
+ not_agents = not_agents or []
422
439
  contains = contains or []
423
440
 
424
441
  Message = cls
@@ -450,6 +467,9 @@ class Message(DeclarativeBase):
450
467
  if topics:
451
468
  query = query.where(or_(*(Message.topic == topic for topic in topics)))
452
469
 
470
+ if agents:
471
+ query = query.where(or_(*(Message.agent_name == agent for agent in agents)))
472
+
453
473
  if contains:
454
474
  query = query.where(or_(*(Message.msg.like(f"%{contain}%") for contain in contains)))
455
475
 
@@ -468,23 +488,86 @@ class Message(DeclarativeBase):
468
488
  if not_topics:
469
489
  query = query.where(not_(or_(*(Message.topic == topic for topic in not_topics))))
470
490
 
491
+ if not_agents:
492
+ query = query.where(not_(or_(*(Message.agent_name == agent for agent in not_agents))))
493
+
494
+ return query
495
+
496
+ @classmethod
497
+ def grep(
498
+ cls,
499
+ *,
500
+ page=1,
501
+ rows_per_page=100,
502
+ order="asc",
503
+ defer=False,
504
+ **kwargs,
505
+ ):
506
+ """Flexible query interface for messages.
507
+
508
+ Arguments are filters. start and end should be :mod:`datetime` objs.
509
+
510
+ Other filters should be lists of strings. They are applied in a
511
+ conjunctive-normal-form (CNF) kind of way
512
+
513
+ for example, the following::
514
+
515
+ users = ['ralph', 'lmacken']
516
+ categories = ['bodhi', 'wiki']
517
+
518
+ should return messages where
519
+
520
+ (user=='ralph' OR user=='lmacken') AND
521
+ (category=='bodhi' OR category=='wiki')
522
+
523
+ Furthermore, you can use a negative version of each argument.
524
+
525
+ users = ['ralph']
526
+ not_categories = ['bodhi', 'wiki']
527
+
528
+ should return messages where
529
+
530
+ (user == 'ralph') AND
531
+ NOT (category == 'bodhi' OR category == 'wiki')
532
+
533
+ ----
534
+
535
+ If the `defer` argument evaluates to True, the query won't actually
536
+ be executed, but a SQLAlchemy query object returned instead.
537
+ """
538
+ query = cls.make_query(**kwargs)
471
539
  # Finally, tag on our pagination arguments
472
- total = session.scalar(query.with_only_columns(func.count(Message.id)))
540
+ Message = cls
541
+
542
+ query_total = query.with_only_columns(func.count(Message.id))
543
+ total = None
473
544
  query = query.order_by(getattr(Message.timestamp, order)())
474
545
 
475
546
  if not rows_per_page:
476
547
  pages = 1
477
548
  else:
549
+ total = session.scalar(query_total)
478
550
  pages = int(math.ceil(total / float(rows_per_page)))
479
551
  query = query.offset(rows_per_page * (page - 1)).limit(rows_per_page)
480
552
 
481
553
  if defer:
482
- return total, page, query
554
+ if total is None:
555
+ total = session.scalar(query_total)
556
+ return total, pages, query
483
557
  else:
484
558
  # Execute!
485
559
  messages = session.scalars(query).all()
560
+ if pages == 1:
561
+ total = len(messages)
486
562
  return total, pages, messages
487
563
 
564
+ @classmethod
565
+ def get_first(cls, *, order="asc", **kwargs):
566
+ """Get the first message matching the regular grep filters."""
567
+ query = cls.make_query(**kwargs)
568
+ query = query.order_by(getattr(Message.timestamp, order)()).limit(1)
569
+ return session.scalars(query).first()
570
+
488
571
 
489
572
  class NamedSingleton:
490
573
  id = Column(Integer, primary_key=True, autoincrement=True)
@@ -0,0 +1,24 @@
1
+ """Message.username → Message.agent_name
2
+
3
+ Revision ID: 429e6f2cba6f
4
+ Revises: 951c40020acc
5
+ Create Date: 2024-06-07 09:12:33.393757
6
+
7
+ """
8
+
9
+ from alembic import op
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = "429e6f2cba6f"
14
+ down_revision = "f6918385051f"
15
+
16
+
17
+ def upgrade():
18
+ op.alter_column("messages", "username", new_column_name="agent_name")
19
+ op.create_index(op.f("ix_messages_agent_name"), "messages", ["agent_name"], unique=False)
20
+
21
+
22
+ def downgrade():
23
+ op.drop_index(op.f("ix_messages_agent_name"), table_name="messages")
24
+ op.alter_column("messages", "agent_name", new_column_name="username")
@@ -0,0 +1,29 @@
1
+ """Messages.headers index
2
+
3
+ Revision ID: f6918385051f
4
+ Revises: 951c40020acc
5
+ Create Date: 2024-05-07 16:05:05.344863
6
+
7
+ """
8
+
9
+ from alembic import op
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = "f6918385051f"
14
+ down_revision = "951c40020acc"
15
+
16
+
17
+ def upgrade():
18
+ op.create_index(
19
+ "ix_messages_headers",
20
+ "messages",
21
+ ["headers"],
22
+ unique=False,
23
+ postgresql_using="gin",
24
+ postgresql_ops={"headers": "jsonb_path_ops"},
25
+ )
26
+
27
+
28
+ def downgrade():
29
+ op.drop_index("ix_messages_headers", table_name="messages", postgresql_using="gin")
@@ -21,8 +21,8 @@ def datanommer_db_url(postgresql_proc):
21
21
  )
22
22
 
23
23
 
24
- @pytest.fixture()
25
- def datanommer_db(postgresql_proc, datanommer_db_url):
24
+ @pytest.fixture(scope="session")
25
+ def datanommer_db_engine(postgresql_proc, datanommer_db_url):
26
26
  with DatabaseJanitor(
27
27
  user=postgresql_proc.user,
28
28
  host=postgresql_proc.host,
@@ -32,12 +32,20 @@ def datanommer_db(postgresql_proc, datanommer_db_url):
32
32
  # template_dbname=postgresql_proc.template_dbname,
33
33
  version=postgresql_proc.version,
34
34
  ):
35
- engine = sa.create_engine(datanommer_db_url, future=True, poolclass=sa.pool.NullPool)
35
+ engine = sa.create_engine(datanommer_db_url, future=True)
36
36
  # Renew the global object, dm.init checks a custom attribute
37
37
  dm.session = scoped_session(dm.maker)
38
38
  dm.init(engine=engine, create=True)
39
39
  yield engine
40
- dm.session.close()
40
+ engine.dispose()
41
+
42
+
43
+ @pytest.fixture()
44
+ def datanommer_db(datanommer_db_url, datanommer_db_engine):
45
+ for table in reversed(dm.DeclarativeBase.metadata.sorted_tables):
46
+ dm.session.execute(table.delete())
47
+ dm.session.commit()
48
+ yield datanommer_db_engine
41
49
 
42
50
 
43
51
  @pytest.fixture()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: datanommer.models
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: SQLAlchemy models for datanommer
5
5
  Home-page: https://github.com/fedora-infra/datanommer
6
6
  License: GPL-3.0-or-later
@@ -32,6 +32,7 @@ Requires-Dist: fmn-messages ; extra == "schemas"
32
32
  Requires-Dist: kerneltest-messages (>=1.0.0,<2.0.0) ; extra == "schemas"
33
33
  Requires-Dist: koji-fedoramessaging-messages (>=1.2.2,<2.0.0) ; extra == "schemas"
34
34
  Requires-Dist: koschei-messages ; extra == "schemas"
35
+ Requires-Dist: maubot-fedora-messages ; extra == "schemas"
35
36
  Requires-Dist: mdapi-messages ; extra == "schemas"
36
37
  Requires-Dist: mediawiki-messages ; extra == "schemas"
37
38
  Requires-Dist: meetbot-messages ; extra == "schemas"
@@ -0,0 +1,12 @@
1
+ datanommer/models/__init__.py,sha256=5By2KNqRy1d7abDFMT3DvhcQZ4UVrx92XokAYevjKnk,19322
2
+ datanommer/models/alembic/env.py,sha256=WNTimgnH70CakhvuV5QCilCnOcjTy7kcx0nD7hryYx0,2793
3
+ datanommer/models/alembic/script.py.mako,sha256=D8kFI44_9vBJZrAYSkZxDTX2-S5Y-oEetEd9BKlo9S8,412
4
+ datanommer/models/alembic/versions/429e6f2cba6f_message_agent_name.py,sha256=JT_q5oweCN8soqMTul1vEspWvqx5TXOfG3ifB9DYmgs,612
5
+ datanommer/models/alembic/versions/5db25abc63be_init.py,sha256=xMD7WGCOqeVNFroCZds_aS_jta2yTrAHc_XhtmZLZRs,249
6
+ datanommer/models/alembic/versions/951c40020acc_unique.py,sha256=GwKDhppKW7y5BUV8BqILZCAiR7GsNyIvoTXUu2A1ZMI,843
7
+ datanommer/models/alembic/versions/f6918385051f_messages_headers_index.py,sha256=vj-yzMOH3MNdQDufPLAxhix74fV_2tzBbEc6JNWt9Og,575
8
+ datanommer/models/testing/__init__.py,sha256=wwAZ-s1U4M7nYNxHgJsn5ZIqLuHMzHrJwGJOwgHAFgc,1693
9
+ datanommer_models-1.4.0.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
10
+ datanommer_models-1.4.0.dist-info/METADATA,sha256=F6nCFWELLinPdPJVmvxwGN4a9wgB7RodZAOlPlA0KLM,2561
11
+ datanommer_models-1.4.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
12
+ datanommer_models-1.4.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- datanommer/models/__init__.py,sha256=ImCJYqPGAPBjx5OoVu6Lbo0mC6Zu9buGfrdgbectsTc,16874
2
- datanommer/models/alembic/env.py,sha256=WNTimgnH70CakhvuV5QCilCnOcjTy7kcx0nD7hryYx0,2793
3
- datanommer/models/alembic/script.py.mako,sha256=D8kFI44_9vBJZrAYSkZxDTX2-S5Y-oEetEd9BKlo9S8,412
4
- datanommer/models/alembic/versions/5db25abc63be_init.py,sha256=xMD7WGCOqeVNFroCZds_aS_jta2yTrAHc_XhtmZLZRs,249
5
- datanommer/models/alembic/versions/951c40020acc_unique.py,sha256=GwKDhppKW7y5BUV8BqILZCAiR7GsNyIvoTXUu2A1ZMI,843
6
- datanommer/models/testing/__init__.py,sha256=T_uy2jBp5TG507WAVTx4A96_SsFgy4UrY6WKOuG_l1Q,1453
7
- datanommer_models-1.2.0.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
8
- datanommer_models-1.2.0.dist-info/METADATA,sha256=KxGFi0ZMyjdK7oslLs8ImIlD9oHogCWF4MXOj2Yyg0k,2502
9
- datanommer_models-1.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
10
- datanommer_models-1.2.0.dist-info/RECORD,,