talentro-commons 0.20.1__py3-none-any.whl → 0.20.3__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.
@@ -203,6 +203,7 @@ class ExternalLinkInfo(BaseModel):
203
203
 
204
204
  link: Optional[LinkInfo]
205
205
  external_id: Optional[str]
206
+ external_url: Optional[str]
206
207
  status: str
207
208
  application_id: UUID
208
209
 
@@ -222,5 +223,6 @@ class ExternalLinkInfo(BaseModel):
222
223
  link=link,
223
224
  status=model.status,
224
225
  external_id=model.external_id,
226
+ external_url=model.external_url,
225
227
  application_id=model.application_id,
226
228
  )
@@ -42,6 +42,7 @@ class Candidate(CandidatesOrganizationModel, table=True):
42
42
  class ExternalLink(CandidatesOrganizationModel, table=True):
43
43
  link_id: Optional[UUID] = Field(index=True, nullable=True)
44
44
  external_id: Optional[str] = Field(index=True, nullable=True)
45
+ external_url: Optional[str] = Field(index=True, nullable=True)
45
46
  status: str = Field(index=True)
46
47
 
47
48
  application_id: UUID = Field(foreign_key="application.id", ondelete="CASCADE", index=True)
File without changes
@@ -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},
64
+ "q.applications.send.integrations": {"enabled": True, "ttl_ms": FIVE_MINUTES},
65
+
66
+ # Acquisitions
67
+ "q.applications.saved.acquisitions": {"enabled": True, "ttl_ms": FIVE_MINUTES},
68
+
69
+ # Vacancies
70
+ "q.applications.saved.vacancies": {"enabled": True, "ttl_ms": FIVE_MINUTES},
71
+
72
+ # Candidates
73
+ "q.applications.normalized.candidates": {"enabled": True, "ttl_ms": FIVE_MINUTES},
74
+ "q.applications.external.upserted.candidates": {"enabled": True, "ttl_ms": FIVE_MINUTES}
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}
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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: talentro-commons
3
- Version: 0.20.1
3
+ Version: 0.20.3
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
@@ -6,8 +6,8 @@ talentro/billing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
6
6
  talentro/billing/dataclasses.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  talentro/billing/models.py,sha256=rbmcJh-FcU5I3oFRhnD413YqIade1nxvqvksvEtCvMs,825
8
8
  talentro/candidates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- talentro/candidates/dataclasses.py,sha256=YHN7qRx1TXgH-zcwkQDB5Ai8saeNSQlNrLvLSKajHoU,6619
10
- talentro/candidates/models.py,sha256=K4MH42ADd_L8i4jBAC35HZFZaHSvIFXS9N8k79CQ7h8,2885
9
+ talentro/candidates/dataclasses.py,sha256=oDlI9-Pv4XAXdJ6aWW42o6mbOGGTSiyOKZy9wE0EbQ0,6696
10
+ talentro/candidates/models.py,sha256=2FktzPjsL8YJw4JTazzby5Q4LX5UdAutaSigIXpJJmc,2952
11
11
  talentro/constants.py,sha256=08CuJ1qVbgi4IDZThw7CQt_D65jGwekGrkMEg8eDGZo,861
12
12
  talentro/event.py,sha256=Xie-nosLwEpg35Hir9yCKtJBXM-_R4O1fyknOtG_6IY,4595
13
13
  talentro/exceptions.py,sha256=-0i7G0-IF3PeWKBMMQ6eq2Hb-Q3OHt-cCqVgxPiWiiQ,1226
@@ -21,13 +21,15 @@ talentro/iam/types.py,sha256=xJjujucc_MJitwPSB3QVFR4c5YZXJLM5cS-ym6rv_dU,206
21
21
  talentro/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  talentro/integrations/dataclasses.py,sha256=FxYbtglKvp_BAufQSCw-IO3wraJUuMvoKZnwJMd5W6w,4482
23
23
  talentro/integrations/models.py,sha256=iNgmAahbonQj2EVd1776xcYzv-1mtAkzGvgDdbjocjo,1292
24
+ talentro/messaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ talentro/messaging/topology.py,sha256=veA1FFjygfw9KcgG0LecMNZyeT_-B6m9AWvwZCYfcFM,3158
24
26
  talentro/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
27
  talentro/services/billing.py,sha256=8pX57-tP7HyRqB_afB-CpEJXiZzMTJyjLyDI3vLvQsI,1989
26
28
  talentro/services/caching.py,sha256=mmjhXAMJ_g8D8cJqn15YqZ7ST-G5lD9MS-PmlowI7pU,2921
27
29
  talentro/services/clients.py,sha256=vluOrdYdDAQLyGR9-EmogLjA9OUlJtHy0tYD9LhwxKg,2174
28
30
  talentro/services/db.py,sha256=cnKCrYG7GwIu7ZZhA25D-yaXaiCJqPpzfcanWquyrBY,822
29
31
  talentro/services/google_storage.py,sha256=5r5uiDZD-76Dylc7yyRG5Ni4XNTc9xK8rC0glCRG8_8,6014
30
- talentro/services/rabbitmq.py,sha256=RNIsLqXM48z5SRRqJ3iWqhRp6FL9TwRwEeii_m8-xDo,6854
32
+ talentro/services/rabbitmq.py,sha256=7qEKBc5Nk2kdjQzQVbyCgPrV3doJ4jIhKmVAWh6QN-k,10745
31
33
  talentro/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
34
  talentro/util/attributes.py,sha256=PgJnn9LMtHkiNIaMov2HQt5944HweD6gRlAHBZrJGPA,448
33
35
  talentro/util/enum.py,sha256=YftsoJKnrn8HAjA2Q1tqIYBMrNnlq6m1b34N8hfykbE,155
@@ -39,6 +41,6 @@ talentro/vacancies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
39
41
  talentro/vacancies/dataclasses.py,sha256=E6H5fsZH4IwtBFSLDolzF6u39tEIrANtWiNVoB65P0c,15074
40
42
  talentro/vacancies/models.py,sha256=GoXr71CNzU6csf8gdmv84etb3Rm-Cdfigp1yqPI_jjQ,4822
41
43
  talentro/vacancies/taxanomy.py,sha256=B6DMq9Wbs0aXFwD9aZveSlLwAC9eq1iCO_KM-FmrV7M,6384
42
- talentro_commons-0.20.1.dist-info/METADATA,sha256=mgvU_osYdjYDXkE2BG1WnnY0zi1rE3SIz1n2OwsWZeU,1507
43
- talentro_commons-0.20.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
44
- talentro_commons-0.20.1.dist-info/RECORD,,
44
+ talentro_commons-0.20.3.dist-info/METADATA,sha256=WjYJUoNIpQ0gWYlxuYzy-VBv3_tZqH1_Q8i2_v77kAA,1507
45
+ talentro_commons-0.20.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
46
+ talentro_commons-0.20.3.dist-info/RECORD,,