jararaca 0.3.11a2__py3-none-any.whl → 0.3.11a4__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 jararaca might be problematic. Click here for more details.
- jararaca/__init__.py +28 -0
- jararaca/cli.py +394 -0
- jararaca/persistence/base.py +2 -1
- jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -13
- {jararaca-0.3.11a2.dist-info → jararaca-0.3.11a4.dist-info}/METADATA +1 -1
- {jararaca-0.3.11a2.dist-info → jararaca-0.3.11a4.dist-info}/RECORD +9 -9
- {jararaca-0.3.11a2.dist-info → jararaca-0.3.11a4.dist-info}/LICENSE +0 -0
- {jararaca-0.3.11a2.dist-info → jararaca-0.3.11a4.dist-info}/WHEEL +0 -0
- {jararaca-0.3.11a2.dist-info → jararaca-0.3.11a4.dist-info}/entry_points.txt +0 -0
jararaca/__init__.py
CHANGED
|
@@ -73,7 +73,11 @@ if TYPE_CHECKING:
|
|
|
73
73
|
from .persistence.interceptors.aiosqa_interceptor import (
|
|
74
74
|
AIOSQAConfig,
|
|
75
75
|
AIOSqlAlchemySessionInterceptor,
|
|
76
|
+
providing_new_session,
|
|
77
|
+
providing_session,
|
|
78
|
+
providing_transaction,
|
|
76
79
|
use_session,
|
|
80
|
+
use_transaction,
|
|
77
81
|
)
|
|
78
82
|
from .persistence.utilities import (
|
|
79
83
|
CriteriaBasedAttributeQueryInjector,
|
|
@@ -191,6 +195,10 @@ if TYPE_CHECKING:
|
|
|
191
195
|
"Container",
|
|
192
196
|
"WebSocketInterceptor",
|
|
193
197
|
"use_session",
|
|
198
|
+
"use_transaction",
|
|
199
|
+
"providing_session",
|
|
200
|
+
"providing_transaction",
|
|
201
|
+
"providing_new_session",
|
|
194
202
|
"Post",
|
|
195
203
|
"Get",
|
|
196
204
|
"Patch",
|
|
@@ -335,6 +343,26 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
335
343
|
"persistence.interceptors.aiosqa_interceptor",
|
|
336
344
|
None,
|
|
337
345
|
),
|
|
346
|
+
"use_transaction": (
|
|
347
|
+
__SPEC_PARENT__,
|
|
348
|
+
"persistence.interceptors.aiosqa_interceptor",
|
|
349
|
+
None,
|
|
350
|
+
),
|
|
351
|
+
"providing_session": (
|
|
352
|
+
__SPEC_PARENT__,
|
|
353
|
+
"persistence.interceptors.aiosqa_interceptor",
|
|
354
|
+
None,
|
|
355
|
+
),
|
|
356
|
+
"providing_new_session": (
|
|
357
|
+
__SPEC_PARENT__,
|
|
358
|
+
"persistence.interceptors.aiosqa_interceptor",
|
|
359
|
+
None,
|
|
360
|
+
),
|
|
361
|
+
"providing_transaction": (
|
|
362
|
+
__SPEC_PARENT__,
|
|
363
|
+
"persistence.interceptors.aiosqa_interceptor",
|
|
364
|
+
None,
|
|
365
|
+
),
|
|
338
366
|
"Post": (__SPEC_PARENT__, "presentation.decorators", None),
|
|
339
367
|
"Get": (__SPEC_PARENT__, "presentation.decorators", None),
|
|
340
368
|
"Patch": (__SPEC_PARENT__, "presentation.decorators", None),
|
jararaca/cli.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import importlib
|
|
2
3
|
import importlib.resources
|
|
3
4
|
import multiprocessing
|
|
@@ -9,6 +10,7 @@ from pathlib import Path
|
|
|
9
10
|
from typing import Any
|
|
10
11
|
from urllib.parse import urlparse, urlunsplit
|
|
11
12
|
|
|
13
|
+
import aio_pika
|
|
12
14
|
import click
|
|
13
15
|
import uvicorn
|
|
14
16
|
from mako.template import Template
|
|
@@ -23,6 +25,7 @@ from jararaca.scheduler.scheduler_v2 import SchedulerV2
|
|
|
23
25
|
from jararaca.tools.typescript.interface_parser import (
|
|
24
26
|
write_microservice_to_typescript_interface,
|
|
25
27
|
)
|
|
28
|
+
from jararaca.utils.rabbitmq_utils import RabbitmqUtils
|
|
26
29
|
|
|
27
30
|
LIBRARY_FILES_PATH = importlib.resources.files("jararaca.files")
|
|
28
31
|
ENTITY_TEMPLATE_PATH = LIBRARY_FILES_PATH / "entity.py.mako"
|
|
@@ -67,6 +70,230 @@ def find_microservice_by_module_path(module_path: str) -> Microservice:
|
|
|
67
70
|
return app
|
|
68
71
|
|
|
69
72
|
|
|
73
|
+
async def declare_worker_infrastructure(
|
|
74
|
+
url: str,
|
|
75
|
+
exchange: str,
|
|
76
|
+
app: Microservice,
|
|
77
|
+
passive_declare: bool = False,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Declare the infrastructure (exchanges and queues) for worker v1.
|
|
81
|
+
"""
|
|
82
|
+
connection = await aio_pika.connect(url)
|
|
83
|
+
channel = await connection.channel()
|
|
84
|
+
|
|
85
|
+
await channel.set_qos(prefetch_count=1)
|
|
86
|
+
|
|
87
|
+
# Declare main exchange
|
|
88
|
+
main_ex = await RabbitmqUtils.declare_main_exchange(
|
|
89
|
+
channel=channel,
|
|
90
|
+
exchange_name=exchange,
|
|
91
|
+
passive=passive_declare,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Declare dead letter infrastructure
|
|
95
|
+
dlx, dlq = await RabbitmqUtils.declare_dl_kit(
|
|
96
|
+
channel=channel, passive=passive_declare
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Find all message handlers to declare their queues
|
|
100
|
+
from jararaca.di import Container
|
|
101
|
+
from jararaca.messagebus.decorators import MessageBusController
|
|
102
|
+
|
|
103
|
+
container = Container(app)
|
|
104
|
+
|
|
105
|
+
for instance_type in app.controllers:
|
|
106
|
+
controller = MessageBusController.get_messagebus(instance_type)
|
|
107
|
+
if controller is None:
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
instance: Any = container.get_by_type(instance_type)
|
|
111
|
+
factory = controller.get_messagebus_factory()
|
|
112
|
+
handlers, _ = factory(instance)
|
|
113
|
+
|
|
114
|
+
for handler in handlers:
|
|
115
|
+
queue_name = f"{handler.message_type.MESSAGE_TOPIC}.{handler.callable.__module__}.{handler.callable.__qualname__}"
|
|
116
|
+
routing_key = f"{handler.message_type.MESSAGE_TOPIC}.#"
|
|
117
|
+
|
|
118
|
+
queue = await channel.declare_queue(
|
|
119
|
+
passive=passive_declare,
|
|
120
|
+
name=queue_name,
|
|
121
|
+
arguments={
|
|
122
|
+
"x-dead-letter-exchange": dlx.name,
|
|
123
|
+
"x-dead-letter-routing-key": dlq.name,
|
|
124
|
+
},
|
|
125
|
+
durable=True,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
await queue.bind(exchange=main_ex, routing_key=routing_key)
|
|
129
|
+
click.echo(f"✓ Declared queue: {queue_name} (routing key: {routing_key})")
|
|
130
|
+
|
|
131
|
+
await channel.close()
|
|
132
|
+
await connection.close()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def declare_worker_v2_infrastructure(
|
|
136
|
+
broker_url: str,
|
|
137
|
+
app: Microservice,
|
|
138
|
+
passive_declare: bool = False,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Declare the infrastructure (exchanges and queues) for worker v2.
|
|
142
|
+
"""
|
|
143
|
+
from urllib.parse import parse_qs, urlparse
|
|
144
|
+
|
|
145
|
+
parsed_url = urlparse(broker_url)
|
|
146
|
+
if parsed_url.scheme not in ["amqp", "amqps"]:
|
|
147
|
+
raise ValueError(f"Unsupported broker URL scheme: {parsed_url.scheme}")
|
|
148
|
+
|
|
149
|
+
if not parsed_url.query:
|
|
150
|
+
raise ValueError("Query string must be set for AMQP URLs")
|
|
151
|
+
|
|
152
|
+
query_params = parse_qs(parsed_url.query)
|
|
153
|
+
|
|
154
|
+
if "exchange" not in query_params or not query_params["exchange"]:
|
|
155
|
+
raise ValueError("Exchange must be set in the query string")
|
|
156
|
+
|
|
157
|
+
exchange = query_params["exchange"][0]
|
|
158
|
+
|
|
159
|
+
connection = await aio_pika.connect(broker_url)
|
|
160
|
+
channel = await connection.channel()
|
|
161
|
+
|
|
162
|
+
await channel.set_qos(prefetch_count=1)
|
|
163
|
+
|
|
164
|
+
# Declare main exchange
|
|
165
|
+
await RabbitmqUtils.declare_main_exchange(
|
|
166
|
+
channel=channel,
|
|
167
|
+
exchange_name=exchange,
|
|
168
|
+
passive=passive_declare,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Declare dead letter infrastructure
|
|
172
|
+
dlx = await RabbitmqUtils.declare_dl_exchange(
|
|
173
|
+
channel=channel, passive=passive_declare
|
|
174
|
+
)
|
|
175
|
+
dlq = await RabbitmqUtils.declare_dl_queue(channel=channel, passive=passive_declare)
|
|
176
|
+
await dlq.bind(dlx, routing_key=RabbitmqUtils.DEAD_LETTER_EXCHANGE)
|
|
177
|
+
|
|
178
|
+
# Find all message handlers and scheduled actions
|
|
179
|
+
from jararaca.di import Container
|
|
180
|
+
from jararaca.messagebus.decorators import MessageBusController
|
|
181
|
+
|
|
182
|
+
container = Container(app)
|
|
183
|
+
|
|
184
|
+
for instance_type in app.controllers:
|
|
185
|
+
controller = MessageBusController.get_messagebus(instance_type)
|
|
186
|
+
if controller is None:
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
instance: Any = container.get_by_type(instance_type)
|
|
190
|
+
factory = controller.get_messagebus_factory()
|
|
191
|
+
handlers, scheduled_actions = factory(instance)
|
|
192
|
+
|
|
193
|
+
# Declare queues for message handlers
|
|
194
|
+
for handler in handlers:
|
|
195
|
+
queue_name = f"{handler.message_type.MESSAGE_TOPIC}.{handler.callable.__module__}.{handler.callable.__qualname__}"
|
|
196
|
+
routing_key = f"{handler.message_type.MESSAGE_TOPIC}.#"
|
|
197
|
+
|
|
198
|
+
queue = await RabbitmqUtils.declare_queue(
|
|
199
|
+
channel=channel, queue_name=queue_name, passive=passive_declare
|
|
200
|
+
)
|
|
201
|
+
await queue.bind(exchange=exchange, routing_key=routing_key)
|
|
202
|
+
click.echo(
|
|
203
|
+
f"✓ Declared message handler queue: {queue_name} (routing key: {routing_key})"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Declare queues for scheduled actions
|
|
207
|
+
for scheduled_action in scheduled_actions:
|
|
208
|
+
queue_name = f"{scheduled_action.callable.__module__}.{scheduled_action.callable.__qualname__}"
|
|
209
|
+
routing_key = queue_name
|
|
210
|
+
|
|
211
|
+
queue = await RabbitmqUtils.declare_queue(
|
|
212
|
+
channel=channel, queue_name=queue_name, passive=passive_declare
|
|
213
|
+
)
|
|
214
|
+
await queue.bind(exchange=exchange, routing_key=routing_key)
|
|
215
|
+
click.echo(
|
|
216
|
+
f"✓ Declared scheduled action queue: {queue_name} (routing key: {routing_key})"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
await channel.close()
|
|
220
|
+
await connection.close()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def declare_scheduler_v2_infrastructure(
|
|
224
|
+
broker_url: str,
|
|
225
|
+
app: Microservice,
|
|
226
|
+
passive_declare: bool = False,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Declare the infrastructure (exchanges and queues) for scheduler v2.
|
|
230
|
+
"""
|
|
231
|
+
from urllib.parse import parse_qs, urlparse
|
|
232
|
+
|
|
233
|
+
from jararaca.scheduler.decorators import ScheduledAction
|
|
234
|
+
|
|
235
|
+
parsed_url = urlparse(broker_url)
|
|
236
|
+
if parsed_url.scheme not in ["amqp", "amqps"]:
|
|
237
|
+
raise ValueError(f"Unsupported broker URL scheme: {parsed_url.scheme}")
|
|
238
|
+
|
|
239
|
+
if not parsed_url.query:
|
|
240
|
+
raise ValueError("Query string must be set for AMQP URLs")
|
|
241
|
+
|
|
242
|
+
query_params = parse_qs(parsed_url.query)
|
|
243
|
+
|
|
244
|
+
if "exchange" not in query_params or not query_params["exchange"]:
|
|
245
|
+
raise ValueError("Exchange must be set in the query string")
|
|
246
|
+
|
|
247
|
+
exchange = query_params["exchange"][0]
|
|
248
|
+
|
|
249
|
+
connection = await aio_pika.connect(broker_url)
|
|
250
|
+
channel = await connection.channel()
|
|
251
|
+
|
|
252
|
+
await channel.set_qos(prefetch_count=1)
|
|
253
|
+
|
|
254
|
+
# Declare exchange for scheduler
|
|
255
|
+
await channel.declare_exchange(
|
|
256
|
+
name=exchange,
|
|
257
|
+
type="topic",
|
|
258
|
+
durable=True,
|
|
259
|
+
auto_delete=False,
|
|
260
|
+
passive=passive_declare,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Find all scheduled actions and declare their queues
|
|
264
|
+
from jararaca.di import Container
|
|
265
|
+
from jararaca.messagebus.decorators import MessageBusController
|
|
266
|
+
|
|
267
|
+
container = Container(app)
|
|
268
|
+
scheduled_actions: list[Any] = []
|
|
269
|
+
|
|
270
|
+
for instance_type in app.controllers:
|
|
271
|
+
controller = MessageBusController.get_messagebus(instance_type)
|
|
272
|
+
if controller is None:
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
instance: Any = container.get_by_type(instance_type)
|
|
276
|
+
factory = controller.get_messagebus_factory()
|
|
277
|
+
_, actions = factory(instance)
|
|
278
|
+
scheduled_actions.extend(actions)
|
|
279
|
+
|
|
280
|
+
for scheduled_action in scheduled_actions:
|
|
281
|
+
queue_name = ScheduledAction.get_function_id(scheduled_action.callable)
|
|
282
|
+
queue = await channel.declare_queue(
|
|
283
|
+
name=queue_name,
|
|
284
|
+
durable=True,
|
|
285
|
+
passive=passive_declare,
|
|
286
|
+
)
|
|
287
|
+
await queue.bind(
|
|
288
|
+
exchange=exchange,
|
|
289
|
+
routing_key=queue_name,
|
|
290
|
+
)
|
|
291
|
+
click.echo(f"✓ Declared scheduler queue: {queue_name}")
|
|
292
|
+
|
|
293
|
+
await channel.close()
|
|
294
|
+
await connection.close()
|
|
295
|
+
|
|
296
|
+
|
|
70
297
|
@click.group()
|
|
71
298
|
def cli() -> None:
|
|
72
299
|
pass
|
|
@@ -493,3 +720,170 @@ def gen_entity(entity_name: str, file_path: StreamWriter) -> None:
|
|
|
493
720
|
entityNameKebabCase=entity_kebab_case,
|
|
494
721
|
)
|
|
495
722
|
)
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
@cli.command("declare-queues-v1")
|
|
726
|
+
@click.argument(
|
|
727
|
+
"app_path",
|
|
728
|
+
type=str,
|
|
729
|
+
)
|
|
730
|
+
@click.option(
|
|
731
|
+
"--broker-url",
|
|
732
|
+
type=str,
|
|
733
|
+
envvar="BROKER_URL",
|
|
734
|
+
help="Broker URL (e.g., amqp://guest:guest@localhost/) [env: BROKER_URL]",
|
|
735
|
+
)
|
|
736
|
+
@click.option(
|
|
737
|
+
"--exchange",
|
|
738
|
+
type=str,
|
|
739
|
+
default="jararaca_ex",
|
|
740
|
+
envvar="EXCHANGE",
|
|
741
|
+
help="Exchange name [env: EXCHANGE]",
|
|
742
|
+
)
|
|
743
|
+
@click.option(
|
|
744
|
+
"--passive-declare",
|
|
745
|
+
is_flag=True,
|
|
746
|
+
default=False,
|
|
747
|
+
help="Use passive declarations (check if infrastructure exists without creating it)",
|
|
748
|
+
)
|
|
749
|
+
def declare_queues_v1(
|
|
750
|
+
app_path: str,
|
|
751
|
+
broker_url: str | None,
|
|
752
|
+
exchange: str,
|
|
753
|
+
passive_declare: bool,
|
|
754
|
+
) -> None:
|
|
755
|
+
"""
|
|
756
|
+
Declare RabbitMQ infrastructure (exchanges and queues) for worker v1.
|
|
757
|
+
|
|
758
|
+
This command pre-declares the necessary exchanges and queues that worker v1
|
|
759
|
+
needs, without starting the actual consumption processes.
|
|
760
|
+
|
|
761
|
+
Environment variables:
|
|
762
|
+
- BROKER_URL: Broker URL (e.g., amqp://guest:guest@localhost/)
|
|
763
|
+
- EXCHANGE: Exchange name (defaults to 'jararaca_ex')
|
|
764
|
+
|
|
765
|
+
Examples:
|
|
766
|
+
|
|
767
|
+
\b
|
|
768
|
+
# Declare worker v1 infrastructure
|
|
769
|
+
jararaca declare-queues-v1 myapp:app --broker-url amqp://guest:guest@localhost/
|
|
770
|
+
|
|
771
|
+
\b
|
|
772
|
+
# Use environment variables
|
|
773
|
+
export BROKER_URL="amqp://guest:guest@localhost/"
|
|
774
|
+
export EXCHANGE="my_exchange"
|
|
775
|
+
jararaca declare-queues-v1 myapp:app
|
|
776
|
+
"""
|
|
777
|
+
|
|
778
|
+
app = find_microservice_by_module_path(app_path)
|
|
779
|
+
|
|
780
|
+
async def run_declarations() -> None:
|
|
781
|
+
if not broker_url:
|
|
782
|
+
click.echo(
|
|
783
|
+
"ERROR: --broker-url is required or set BROKER_URL environment variable",
|
|
784
|
+
err=True,
|
|
785
|
+
)
|
|
786
|
+
return
|
|
787
|
+
|
|
788
|
+
click.echo(
|
|
789
|
+
f"→ Declaring worker v1 infrastructure (URL: {broker_url}, Exchange: {exchange})"
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
try:
|
|
793
|
+
await declare_worker_infrastructure(
|
|
794
|
+
broker_url, exchange, app, passive_declare
|
|
795
|
+
)
|
|
796
|
+
click.echo("✓ Worker v1 infrastructure declared successfully!")
|
|
797
|
+
except Exception as e:
|
|
798
|
+
click.echo(
|
|
799
|
+
f"ERROR: Failed to declare worker v1 infrastructure: {e}", err=True
|
|
800
|
+
)
|
|
801
|
+
raise
|
|
802
|
+
|
|
803
|
+
asyncio.run(run_declarations())
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
@cli.command("declare-queues-v2")
|
|
807
|
+
@click.argument(
|
|
808
|
+
"app_path",
|
|
809
|
+
type=str,
|
|
810
|
+
)
|
|
811
|
+
@click.option(
|
|
812
|
+
"--broker-url",
|
|
813
|
+
type=str,
|
|
814
|
+
envvar="BROKER_URL",
|
|
815
|
+
help="Broker URL (e.g., amqp://guest:guest@localhost/) [env: BROKER_URL]",
|
|
816
|
+
)
|
|
817
|
+
@click.option(
|
|
818
|
+
"--exchange",
|
|
819
|
+
type=str,
|
|
820
|
+
default="jararaca_ex",
|
|
821
|
+
envvar="EXCHANGE",
|
|
822
|
+
help="Exchange name [env: EXCHANGE]",
|
|
823
|
+
)
|
|
824
|
+
@click.option(
|
|
825
|
+
"--passive-declare",
|
|
826
|
+
is_flag=True,
|
|
827
|
+
default=False,
|
|
828
|
+
help="Use passive declarations (check if infrastructure exists without creating it)",
|
|
829
|
+
)
|
|
830
|
+
def declare_queues_v2(
|
|
831
|
+
app_path: str,
|
|
832
|
+
broker_url: str | None,
|
|
833
|
+
exchange: str,
|
|
834
|
+
passive_declare: bool,
|
|
835
|
+
) -> None:
|
|
836
|
+
"""
|
|
837
|
+
Declare RabbitMQ infrastructure (exchanges and queues) for worker v2 and scheduler v2.
|
|
838
|
+
|
|
839
|
+
This command pre-declares the necessary exchanges and queues that worker v2
|
|
840
|
+
and scheduler v2 need, without starting the actual consumption processes.
|
|
841
|
+
|
|
842
|
+
Environment variables:
|
|
843
|
+
- BROKER_URL: Broker URL (e.g., amqp://guest:guest@localhost/)
|
|
844
|
+
- EXCHANGE: Exchange name (defaults to 'jararaca_ex')
|
|
845
|
+
|
|
846
|
+
Examples:
|
|
847
|
+
|
|
848
|
+
\b
|
|
849
|
+
# Declare worker v2 and scheduler v2 infrastructure
|
|
850
|
+
jararaca declare-queues-v2 myapp:app --broker-url amqp://guest:guest@localhost/
|
|
851
|
+
|
|
852
|
+
\b
|
|
853
|
+
# Use environment variables
|
|
854
|
+
export BROKER_URL="amqp://guest:guest@localhost/"
|
|
855
|
+
export EXCHANGE="my_exchange"
|
|
856
|
+
jararaca declare-queues-v2 myapp:app
|
|
857
|
+
"""
|
|
858
|
+
|
|
859
|
+
app = find_microservice_by_module_path(app_path)
|
|
860
|
+
|
|
861
|
+
async def run_declarations() -> None:
|
|
862
|
+
if not broker_url:
|
|
863
|
+
click.echo(
|
|
864
|
+
"ERROR: --broker-url is required or set BROKER_URL environment variable",
|
|
865
|
+
err=True,
|
|
866
|
+
)
|
|
867
|
+
return
|
|
868
|
+
|
|
869
|
+
# For v2, create the broker URL with exchange parameter
|
|
870
|
+
v2_broker_url = f"{broker_url}?exchange={exchange}"
|
|
871
|
+
|
|
872
|
+
click.echo(f"→ Declaring worker v2 infrastructure (URL: {v2_broker_url})")
|
|
873
|
+
click.echo(f"→ Declaring scheduler v2 infrastructure (URL: {v2_broker_url})")
|
|
874
|
+
|
|
875
|
+
try:
|
|
876
|
+
await asyncio.gather(
|
|
877
|
+
declare_worker_v2_infrastructure(v2_broker_url, app, passive_declare),
|
|
878
|
+
declare_scheduler_v2_infrastructure(
|
|
879
|
+
v2_broker_url, app, passive_declare
|
|
880
|
+
),
|
|
881
|
+
)
|
|
882
|
+
click.echo(
|
|
883
|
+
"✓ Worker v2 and scheduler v2 infrastructure declared successfully!"
|
|
884
|
+
)
|
|
885
|
+
except Exception as e:
|
|
886
|
+
click.echo(f"ERROR: Failed to declare v2 infrastructure: {e}", err=True)
|
|
887
|
+
raise
|
|
888
|
+
|
|
889
|
+
asyncio.run(run_declarations())
|
jararaca/persistence/base.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Any, Self, Type, TypeVar
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncAttrs
|
|
4
5
|
from sqlalchemy.orm import DeclarativeBase
|
|
5
6
|
|
|
6
7
|
IDENTIFIABLE_SCHEMA_T = TypeVar("IDENTIFIABLE_SCHEMA_T")
|
|
@@ -20,7 +21,7 @@ def recursive_get_dict(obj: Any) -> Any:
|
|
|
20
21
|
return obj
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
class BaseEntity(DeclarativeBase):
|
|
24
|
+
class BaseEntity(AsyncAttrs, DeclarativeBase):
|
|
24
25
|
|
|
25
26
|
@classmethod
|
|
26
27
|
def from_basemodel(cls, mutation: T_BASEMODEL) -> "Self":
|
|
@@ -3,21 +3,47 @@ from contextvars import ContextVar
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any, AsyncGenerator, Generator
|
|
5
5
|
|
|
6
|
-
from sqlalchemy.ext.asyncio import
|
|
6
|
+
from sqlalchemy.ext.asyncio import (
|
|
7
|
+
AsyncSession,
|
|
8
|
+
AsyncSessionTransaction,
|
|
9
|
+
async_sessionmaker,
|
|
10
|
+
create_async_engine,
|
|
11
|
+
)
|
|
7
12
|
from sqlalchemy.ext.asyncio.engine import AsyncEngine
|
|
8
13
|
|
|
9
14
|
from jararaca.microservice import AppContext, AppInterceptor
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
ctx_default_connection_name: ContextVar[str] = ContextVar("ctx_default_connection_name")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ensure_name(name: str | None) -> str:
|
|
20
|
+
if name is None:
|
|
21
|
+
return ctx_default_connection_name.get()
|
|
22
|
+
return name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class PersistenceCtx:
|
|
27
|
+
session: AsyncSession
|
|
28
|
+
tx: AsyncSessionTransaction
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ctx_session_map = ContextVar[dict[str, PersistenceCtx]]("ctx_session_map", default={})
|
|
12
32
|
|
|
13
33
|
|
|
14
34
|
@contextmanager
|
|
15
|
-
def
|
|
16
|
-
|
|
35
|
+
def providing_session(
|
|
36
|
+
session: AsyncSession,
|
|
37
|
+
tx: AsyncSessionTransaction,
|
|
38
|
+
connection_name: str | None = None,
|
|
17
39
|
) -> Generator[None, Any, None]:
|
|
40
|
+
|
|
41
|
+
connection_name = ensure_name(connection_name)
|
|
18
42
|
current_map = ctx_session_map.get({})
|
|
19
43
|
|
|
20
|
-
token = ctx_session_map.set(
|
|
44
|
+
token = ctx_session_map.set(
|
|
45
|
+
{**current_map, connection_name: PersistenceCtx(session, tx)}
|
|
46
|
+
)
|
|
21
47
|
|
|
22
48
|
try:
|
|
23
49
|
yield
|
|
@@ -26,18 +52,60 @@ def provide_session(
|
|
|
26
52
|
ctx_session_map.reset(token)
|
|
27
53
|
|
|
28
54
|
|
|
29
|
-
|
|
55
|
+
@asynccontextmanager
|
|
56
|
+
async def providing_new_session(
|
|
57
|
+
connection_name: str | None = None,
|
|
58
|
+
) -> AsyncGenerator[AsyncSession, None]:
|
|
59
|
+
|
|
60
|
+
current_session = use_session(connection_name)
|
|
61
|
+
|
|
62
|
+
async with AsyncSession(
|
|
63
|
+
current_session.bind,
|
|
64
|
+
) as new_session, new_session.begin() as new_tx:
|
|
65
|
+
with providing_session(new_session, new_tx, connection_name):
|
|
66
|
+
yield new_session
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def use_session(connection_name: str | None = None) -> AsyncSession:
|
|
70
|
+
connection_name = ensure_name(connection_name)
|
|
30
71
|
current_map = ctx_session_map.get({})
|
|
31
72
|
if connection_name not in current_map:
|
|
32
73
|
raise ValueError(f"Session not found for connection {connection_name}")
|
|
33
74
|
|
|
34
|
-
return current_map[connection_name]
|
|
75
|
+
return current_map[connection_name].session
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@contextmanager
|
|
79
|
+
def providing_transaction(
|
|
80
|
+
tx: AsyncSessionTransaction,
|
|
81
|
+
connection_name: str | None = None,
|
|
82
|
+
) -> Generator[None, Any, None]:
|
|
83
|
+
connection_name = ensure_name(connection_name)
|
|
84
|
+
|
|
85
|
+
current_map = ctx_session_map.get({})
|
|
86
|
+
|
|
87
|
+
if connection_name not in current_map:
|
|
88
|
+
raise ValueError(f"No session found for connection {connection_name}")
|
|
89
|
+
|
|
90
|
+
with providing_session(current_map[connection_name].session, tx, connection_name):
|
|
91
|
+
yield
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def use_transaction(connection_name: str | None = None) -> AsyncSessionTransaction:
|
|
95
|
+
current_map = ctx_session_map.get({})
|
|
96
|
+
if connection_name not in current_map:
|
|
97
|
+
raise ValueError(f"Transaction not found for connection {connection_name}")
|
|
98
|
+
|
|
99
|
+
return current_map[connection_name].tx
|
|
35
100
|
|
|
36
101
|
|
|
37
|
-
@dataclass
|
|
38
102
|
class AIOSQAConfig:
|
|
39
|
-
connection_name: str
|
|
40
103
|
url: str | AsyncEngine
|
|
104
|
+
connection_name: str
|
|
105
|
+
|
|
106
|
+
def __init__(self, url: str | AsyncEngine, connection_name: str = "default"):
|
|
107
|
+
self.url = url
|
|
108
|
+
self.connection_name = connection_name
|
|
41
109
|
|
|
42
110
|
|
|
43
111
|
class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
@@ -54,11 +122,16 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
|
54
122
|
|
|
55
123
|
@asynccontextmanager
|
|
56
124
|
async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
|
|
57
|
-
|
|
58
|
-
|
|
125
|
+
|
|
126
|
+
async with self.sessionmaker() as session, session.begin() as tx:
|
|
127
|
+
token = ctx_default_connection_name.set(self.config.connection_name)
|
|
128
|
+
with providing_session(session, tx, self.config.connection_name):
|
|
59
129
|
try:
|
|
60
130
|
yield
|
|
61
|
-
|
|
131
|
+
if tx.is_active:
|
|
132
|
+
await tx.commit()
|
|
62
133
|
except Exception as e:
|
|
63
|
-
await
|
|
134
|
+
await tx.rollback()
|
|
64
135
|
raise e
|
|
136
|
+
finally:
|
|
137
|
+
ctx_default_connection_name.reset(token)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
jararaca/__init__.py,sha256=
|
|
1
|
+
jararaca/__init__.py,sha256=LL97aPXyHyb-bjW4EYL2y33UjJER2GlvjMlZiTLioEw,16387
|
|
2
2
|
jararaca/__main__.py,sha256=-O3vsB5lHdqNFjUtoELDF81IYFtR-DSiiFMzRaiSsv4,67
|
|
3
3
|
jararaca/broker_backend/__init__.py,sha256=GzEIuHR1xzgCJD4FE3harNjoaYzxHMHoEL0_clUaC-k,3528
|
|
4
4
|
jararaca/broker_backend/mapper.py,sha256=vTsi7sWpNvlga1PWPFg0rCJ5joJ0cdzykkIc2Tuvenc,696
|
|
5
5
|
jararaca/broker_backend/redis_broker_backend.py,sha256=a7DHchy3NAiD71Ix8SwmQOUnniu7uup-Woa4ON_4J7I,5786
|
|
6
|
-
jararaca/cli.py,sha256=
|
|
6
|
+
jararaca/cli.py,sha256=9TrlHdZGaqNO3Hg4HpBsTc9x8W3oFP9fVqjPR0rMzKk,24671
|
|
7
7
|
jararaca/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
jararaca/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
jararaca/core/providers.py,sha256=wktH84FK7c1s2wNq-fudf1uMfi3CQBR0neU2czJ_L0U,434
|
|
@@ -27,10 +27,10 @@ jararaca/observability/decorators.py,sha256=XffBinFXdiNkY6eo8_1nkr_GapM0RUGBg0ai
|
|
|
27
27
|
jararaca/observability/interceptor.py,sha256=GHkuGKFWftN7MDjvYeGFGEPnuJETNhtxRK6yuPrCrpU,1462
|
|
28
28
|
jararaca/observability/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
29
|
jararaca/observability/providers/otel.py,sha256=LgfoITdoQTCxKebfLcEfwMiG992wlWY_0AUTd2fo8hY,6065
|
|
30
|
-
jararaca/persistence/base.py,sha256=
|
|
30
|
+
jararaca/persistence/base.py,sha256=xnGUbsLNz3gO-9iJt-Sn5NY13Yc9-misP8wLwQuGGoM,1024
|
|
31
31
|
jararaca/persistence/exports.py,sha256=Ghx4yoFaB4QVTb9WxrFYgmcSATXMNvrOvT8ybPNKXCA,62
|
|
32
32
|
jararaca/persistence/interceptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
-
jararaca/persistence/interceptors/aiosqa_interceptor.py,sha256=
|
|
33
|
+
jararaca/persistence/interceptors/aiosqa_interceptor.py,sha256=VOaoSFZtcCJTrdYxjLUtzkG6bWMVbOKT6WI1BP2hmls,4090
|
|
34
34
|
jararaca/persistence/session.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
35
|
jararaca/persistence/sort_filter.py,sha256=agggpN0YvNjUr6wJjy69NkaqxoDDW13ys9B3r85OujA,9226
|
|
36
36
|
jararaca/persistence/utilities.py,sha256=imcV4Oi5kXNk6m9QF2-OsnFpcTRY4w5mBYLdEx5XTSQ,14296
|
|
@@ -66,8 +66,8 @@ jararaca/tools/metadata.py,sha256=7nlCDYgItNybentPSSCc2MLqN7IpBd0VyQzfjfQycVI,14
|
|
|
66
66
|
jararaca/tools/typescript/interface_parser.py,sha256=35xbOrZDQDyTXdMrVZQ8nnFw79f28lJuLYNHAspIqi8,30492
|
|
67
67
|
jararaca/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
68
|
jararaca/utils/rabbitmq_utils.py,sha256=FPDP8ZVgvitZXV-oK73D7EIANsqUzXTW7HdpEKsIsyI,2811
|
|
69
|
-
jararaca-0.3.
|
|
70
|
-
jararaca-0.3.
|
|
71
|
-
jararaca-0.3.
|
|
72
|
-
jararaca-0.3.
|
|
73
|
-
jararaca-0.3.
|
|
69
|
+
jararaca-0.3.11a4.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
70
|
+
jararaca-0.3.11a4.dist-info/METADATA,sha256=VeC353GhTULAEFtJysdLpvMX1MBxRhwXsL35n4pCm5k,4954
|
|
71
|
+
jararaca-0.3.11a4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
72
|
+
jararaca-0.3.11a4.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
|
|
73
|
+
jararaca-0.3.11a4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|