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.
- talentro/candidates/dataclasses.py +2 -0
- talentro/candidates/models.py +1 -0
- talentro/messaging/__init__.py +0 -0
- talentro/messaging/topology.py +103 -0
- talentro/services/rabbitmq.py +109 -17
- {talentro_commons-0.20.1.dist-info → talentro_commons-0.20.3.dist-info}/METADATA +1 -1
- {talentro_commons-0.20.1.dist-info → talentro_commons-0.20.3.dist-info}/RECORD +8 -6
- {talentro_commons-0.20.1.dist-info → talentro_commons-0.20.3.dist-info}/WHEEL +0 -0
|
@@ -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
|
)
|
talentro/candidates/models.py
CHANGED
|
@@ -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
|
+
)
|
talentro/services/rabbitmq.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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):
|
|
@@ -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=
|
|
10
|
-
talentro/candidates/models.py,sha256=
|
|
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=
|
|
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.
|
|
43
|
-
talentro_commons-0.20.
|
|
44
|
-
talentro_commons-0.20.
|
|
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,,
|
|
File without changes
|