talentro-commons 0.20.2__tar.gz → 0.20.4__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.
Files changed (46) hide show
  1. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/PKG-INFO +1 -1
  2. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/pyproject.toml +1 -1
  3. talentro_commons-0.20.4/src/talentro/messaging/topology.py +103 -0
  4. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/services/rabbitmq.py +109 -17
  5. talentro_commons-0.20.4/src/talentro/vacancies/__init__.py +0 -0
  6. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/README.md +0 -0
  7. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/__init__.py +0 -0
  8. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/acquisition/__init__.py +0 -0
  9. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/acquisition/dataclasses.py +0 -0
  10. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/acquisition/models.py +0 -0
  11. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/billing/__init__.py +0 -0
  12. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/billing/dataclasses.py +0 -0
  13. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/billing/models.py +0 -0
  14. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/candidates/__init__.py +0 -0
  15. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/candidates/dataclasses.py +0 -0
  16. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/candidates/models.py +0 -0
  17. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/constants.py +0 -0
  18. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/event.py +0 -0
  19. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/exceptions.py +0 -0
  20. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/general/__init__.py +0 -0
  21. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/general/dataclasses.py +0 -0
  22. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/general/models.py +0 -0
  23. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/iam/__init__.py +0 -0
  24. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/iam/dataclasses.py +0 -0
  25. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/iam/models.py +0 -0
  26. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/iam/types.py +0 -0
  27. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/integrations/__init__.py +0 -0
  28. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/integrations/dataclasses.py +0 -0
  29. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/integrations/models.py +0 -0
  30. {talentro_commons-0.20.2/src/talentro/services → talentro_commons-0.20.4/src/talentro/messaging}/__init__.py +0 -0
  31. {talentro_commons-0.20.2/src/talentro/util → talentro_commons-0.20.4/src/talentro/services}/__init__.py +0 -0
  32. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/services/billing.py +0 -0
  33. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/services/caching.py +0 -0
  34. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/services/clients.py +0 -0
  35. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/services/db.py +0 -0
  36. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/services/google_storage.py +0 -0
  37. {talentro_commons-0.20.2/src/talentro/vacancies → talentro_commons-0.20.4/src/talentro/util}/__init__.py +0 -0
  38. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/util/attributes.py +0 -0
  39. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/util/enum.py +0 -0
  40. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/util/files.py +0 -0
  41. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/util/singleton.py +0 -0
  42. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/util/string.py +0 -0
  43. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/util/vacancy.py +0 -0
  44. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/vacancies/dataclasses.py +0 -0
  45. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/vacancies/models.py +0 -0
  46. {talentro_commons-0.20.2 → talentro_commons-0.20.4}/src/talentro/vacancies/taxanomy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: talentro-commons
3
- Version: 0.20.2
3
+ Version: 0.20.4
4
4
  Summary: This package contains all globally used code, services, models and data structures for Talentro
5
5
  License: Proprietary
6
6
  Author: Emiel van Essen
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "talentro-commons"
3
- version = "0.20.2"
3
+ version = "0.20.4"
4
4
  description = "This package contains all globally used code, services, models and data structures for Talentro"
5
5
  authors = ["Emiel van Essen <emiel@marksmen.nl>"]
6
6
  license = "Proprietary"
@@ -0,0 +1,103 @@
1
+ import aio_pika
2
+
3
+ from typing import List, Tuple, Optional
4
+
5
+ FIVE_MINUTES = 5 * 60 * 1000
6
+ FIFTEEN_MINUTES = 15 * 60 * 1000
7
+
8
+ class TopologyConfig:
9
+ def __init__(
10
+ self,
11
+ exchange_name: str,
12
+ exchange_type: aio_pika.ExchangeType,
13
+ bindings: List[Tuple[str, str]],
14
+ dlx_name: str = None,
15
+ retry_config: Optional[dict] = None
16
+ ):
17
+ self.exchange_name = exchange_name
18
+ self.exchange_type = exchange_type
19
+ self.bindings = bindings
20
+ self.dlx_name = dlx_name
21
+ self.retry_config = retry_config
22
+
23
+ def get_max_retries(self, queue_name: str) -> int:
24
+ """Get max retries for a specific queue, default 3."""
25
+ if not self.retry_config:
26
+ return 3
27
+ return self.retry_config.get(queue_name, {}).get("max_retries", 3)
28
+
29
+
30
+ FRONTEND_TOPOLOGY = TopologyConfig(
31
+ exchange_name="x.frontend",
32
+ exchange_type=aio_pika.ExchangeType.TOPIC,
33
+ bindings=[
34
+ ("q.frontend.updates.gateway", "frontend.update"),
35
+ ],
36
+ retry_config={
37
+ "q.frontend.updates.gateway": {"enabled": False}
38
+ }
39
+ )
40
+
41
+
42
+ APPLICATIONS_TOPOLOGY = TopologyConfig(
43
+ exchange_name="x.applications",
44
+ exchange_type=aio_pika.ExchangeType.TOPIC,
45
+ bindings=[
46
+ # Integrations
47
+ ("q.applications.raw.integrations", "applications.raw"),
48
+ ("q.applications.send.integrations", "applications.send"),
49
+
50
+ # Acquisitions
51
+ ("q.applications.saved.acquisitions", "applications.saved"),
52
+
53
+ # Vacancies
54
+ ("q.applications.saved.vacancies", "applications.saved"),
55
+
56
+ # Candidates
57
+ ("q.applications.normalized.candidates", "applications.normalized"),
58
+ ("q.applications.external.upserted.candidates", "applications.external.upserted"),
59
+ ],
60
+ dlx_name="x.applications.dlx",
61
+ retry_config={
62
+ # Integrations
63
+ "q.applications.raw.integrations": {"enabled": True, "ttl_ms": FIVE_MINUTES, "max_retries": 3},
64
+ "q.applications.send.integrations": {"enabled": True, "ttl_ms": FIVE_MINUTES, "max_retries": 3},
65
+
66
+ # Acquisitions
67
+ "q.applications.saved.acquisitions": {"enabled": True, "ttl_ms": FIVE_MINUTES, "max_retries": 3},
68
+
69
+ # Vacancies
70
+ "q.applications.saved.vacancies": {"enabled": True, "ttl_ms": FIVE_MINUTES, "max_retries": 3},
71
+
72
+ # Candidates
73
+ "q.applications.normalized.candidates": {"enabled": True, "ttl_ms": FIVE_MINUTES, "max_retries": 3},
74
+ "q.applications.external.upserted.candidates": {"enabled": True, "ttl_ms": FIVE_MINUTES, "max_retries": 3}
75
+ }
76
+ )
77
+
78
+
79
+ FEEDS_TOPOLOGY = TopologyConfig(
80
+ exchange_name="x.feeds",
81
+ exchange_type=aio_pika.ExchangeType.TOPIC,
82
+ bindings=[
83
+ # Acquisitions
84
+ ("q.feeds.sync.acquisitions", "feeds.sync"),
85
+ ],
86
+ dlx_name="x.feeds.dlx",
87
+ retry_config={
88
+ "q.feeds.sync.acquisitions": {"enabled": True, "ttl_ms": FIVE_MINUTES, "max_retries": 3}
89
+ }
90
+ )
91
+
92
+ BILLING_TOPOLOGY = TopologyConfig(
93
+ exchange_name="x.billing",
94
+ exchange_type=aio_pika.ExchangeType.TOPIC,
95
+ bindings=[
96
+ # Billing
97
+ ("q.billing.events.billing", "billing.event"),
98
+ ],
99
+ dlx_name="x.billing.dlx",
100
+ retry_config={
101
+ "q.billing.events.billing": {"enabled": True, "ttl_ms": FIVE_MINUTES, "max_retries": 3}
102
+ }
103
+ )
@@ -1,11 +1,14 @@
1
1
  import asyncio
2
2
  import os
3
+ import traceback
3
4
  from typing import List, Tuple, Optional, Callable
4
5
 
5
6
  import aio_pika
6
7
  from aiormq import AMQPConnectionError
7
8
 
8
9
  from ..event import Message, Event
10
+ from ..messaging.topology import TopologyConfig, FRONTEND_TOPOLOGY, BILLING_TOPOLOGY, FEEDS_TOPOLOGY, \
11
+ APPLICATIONS_TOPOLOGY
9
12
  from ..util.singleton import SingletonMeta
10
13
 
11
14
 
@@ -43,7 +46,8 @@ class RabbitMQ:
43
46
  exchange_type: aio_pika.ExchangeType = aio_pika.ExchangeType.TOPIC,
44
47
  durable: bool = True,
45
48
  bindings: List[Tuple[str, str]] = (),
46
- dlx_name: Optional[str] = None
49
+ dlx_name: Optional[str] = None,
50
+ retry_config: Optional[dict] = None # { "queue_name": {"retry_enabled": True, "ttl_ms": 300000}, ... }
47
51
  ):
48
52
  exchange = await self.channel.declare_exchange(
49
53
  exchange_name, exchange_type, durable=durable
@@ -73,7 +77,36 @@ class RabbitMQ:
73
77
  await dlq.bind(dlx_name, routing_key=f"{queue_name}.dead")
74
78
  print(f" RabbitMQ - DLQ bound: {dlq.name} ← {dlx_name}")
75
79
 
76
- async def consume(self, exchange_name: str, queue: str, callback: Callable, dlx_name: Optional[str] = None):
80
+ queue_retry_config = retry_config.get(queue_name, {})
81
+ if queue_retry_config.get("enabled", False):
82
+ ttl_ms = queue_retry_config.get("ttl_ms", 300000)
83
+
84
+ retry_exchange = await self.channel.declare_exchange(
85
+ f"{dlx_name}.retry", aio_pika.ExchangeType.DIRECT, durable=True
86
+ )
87
+
88
+ retry_queue_args = {
89
+ "x-dead-letter-exchange": exchange_name,
90
+ "x-dead-letter-routing-key": queue_name,
91
+ "x-message-ttl": ttl_ms
92
+ }
93
+ retry_queue = await self.channel.declare_queue(
94
+ f"{queue_name}.retry",
95
+ durable=True,
96
+ arguments=retry_queue_args
97
+ )
98
+ await retry_queue.bind(retry_exchange, routing_key=f"{queue_name}.retry")
99
+ print(f" RabbitMQ - Retry queue enabled: {retry_queue.name} "
100
+ f"(TTL: {ttl_ms}ms)")
101
+
102
+ async def consume(
103
+ self,
104
+ exchange_name: str,
105
+ queue: str,
106
+ callback: Callable,
107
+ dlx_name: Optional[str] = None,
108
+ max_retries: int = 3
109
+ ):
77
110
  while True:
78
111
  try:
79
112
  print(f" RabbitMQ - Consuming from queue: {queue}")
@@ -91,10 +124,17 @@ class RabbitMQ:
91
124
  await queue_object.bind(exchange, routing_key=queue)
92
125
 
93
126
  async for message in queue_object:
94
- async with message.process():
95
- event: Event = Event.decode(message.body)
96
- print(f" RabbitMQ ({queue}) - [x] Received event: {event.meta.event_type} (trace: {event.meta.trace_id})")
97
- await callback(queue, event)
127
+ try:
128
+ async with message.process():
129
+ event: Event = Event.decode(message.body)
130
+ print(f" RabbitMQ ({queue}) - [x] Received event: {event.meta.event_type} (trace: {event.meta.trace_id})")
131
+ await callback(queue, event)
132
+ except Exception as e:
133
+ # Handle callback exception - send to retry queue
134
+ print(f" RabbitMQ ({queue}) - ⚠️ Error processing message: {e}")
135
+ traceback.print_exc()
136
+ await self._handle_failed_message(message, queue, exchange_name, dlx_name, max_retries)
137
+
98
138
  except (aio_pika.exceptions.AMQPConnectionError, aio_pika.exceptions.ChannelClosed):
99
139
  print(f" RabbitMQ - Connection lost while consuming {queue}. Retrying in 5 seconds...")
100
140
  await asyncio.sleep(5)
@@ -102,6 +142,45 @@ class RabbitMQ:
102
142
  print(f" RabbitMQ - Unexpected error in consumer {queue}: {e}")
103
143
  await asyncio.sleep(5)
104
144
 
145
+ @staticmethod
146
+ async def _handle_failed_message(
147
+ message: aio_pika.IncomingMessage,
148
+ queue_name: str,
149
+ exchange_name: str,
150
+ dlx_name: Optional[str] = None,
151
+ max_retries: int = 3
152
+ ):
153
+ """
154
+ Handle failed messages by sending them to the retry queue or DLQ.
155
+ """
156
+ try:
157
+ # Get death count from headers
158
+ x_death = message.headers.get('x-death', [])
159
+ death_count = 0
160
+
161
+ if x_death:
162
+ # x-death is a list of death info dicts
163
+ death_count = len(x_death)
164
+
165
+ print(f" RabbitMQ ({queue_name}) - Retry attempt {death_count + 1}/{max_retries + 1}")
166
+
167
+ if death_count >= max_retries:
168
+ # Max retries exceeded - send to DLQ
169
+ print(f" RabbitMQ ({queue_name}) - ❌ Max retries ({max_retries}) exceeded. Sending to DLQ.")
170
+ await message.nack(requeue=False)
171
+ else:
172
+ # Still have retries left - send to retry queue
173
+ if dlx_name:
174
+ await message.nack(requeue=False)
175
+ print(f" RabbitMQ ({queue_name}) - Sending to retry queue...")
176
+ else:
177
+ await message.nack(requeue=True)
178
+ print(f" RabbitMQ ({queue_name}) - Message requeued")
179
+
180
+ except Exception as e:
181
+ print(f" RabbitMQ ({queue_name}) - Error handling failed message: {e}")
182
+ await message.nack(requeue=False)
183
+
105
184
  async def send_message(self, message: Message):
106
185
  # Haal de exchange op
107
186
  if message.exchange == "default":
@@ -131,6 +210,7 @@ class QueueContext(metaclass=SingletonMeta):
131
210
  def __init__(self):
132
211
  self._rabbit_mq = RabbitMQ()
133
212
  self._message_callbacks = []
213
+ self._topology_config: TopologyConfig | None = None
134
214
 
135
215
  async def connect(self):
136
216
  try:
@@ -140,22 +220,34 @@ class QueueContext(metaclass=SingletonMeta):
140
220
  await asyncio.sleep(5)
141
221
  await self.connect()
142
222
 
223
+ async def setup_topologies(self):
224
+ topologies = [FRONTEND_TOPOLOGY, APPLICATIONS_TOPOLOGY, FEEDS_TOPOLOGY, BILLING_TOPOLOGY]
225
+
226
+ for topology in topologies:
227
+ await self._setup_topology_with_config(topology)
228
+
229
+ async def _setup_topology_with_config(self, topology_config: TopologyConfig):
230
+ self._topology_config = topology_config
143
231
 
144
- async def setup_topology(
145
- self,
146
- exchange_name: str,
147
- exchange_type: aio_pika.ExchangeType = aio_pika.ExchangeType.TOPIC,
148
- durable: bool = True,
149
- bindings: List[Tuple[str, str]] = (),
150
- dlx_name: Optional[str] = None
151
- ):
152
232
  await self._rabbit_mq.setup_topology(
153
- exchange_name, exchange_type, durable, bindings, dlx_name
233
+ topology_config.exchange_name,
234
+ topology_config.exchange_type,
235
+ bindings=topology_config.bindings,
236
+ dlx_name=topology_config.dlx_name,
237
+ retry_config=topology_config.retry_config
154
238
  )
155
239
 
156
- async def start_consuming(self, exchange_name: str, queue: str, dlx_name: Optional[str] = None):
240
+ async def start_consuming(self, queue: str):
241
+ max_retries = self._topology_config.get_max_retries(queue) if self._topology_config else 3
242
+
157
243
  asyncio.create_task(
158
- self._rabbit_mq.consume(exchange_name, queue, self._on_new_message, dlx_name=dlx_name)
244
+ self._rabbit_mq.consume(
245
+ self._topology_config.exchange_name,
246
+ queue,
247
+ self._on_new_message,
248
+ dlx_name=self._topology_config.dlx_name,
249
+ max_retries=max_retries
250
+ )
159
251
  )
160
252
 
161
253
  async def _on_new_message(self, queue: str, event: Event):