orca-python 0.2.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.
orca_python/main.py ADDED
@@ -0,0 +1,686 @@
1
+ """
2
+ Orca Python SDK
3
+
4
+ This SDK provides the `Processor` class, which integrates with the Orca gRPC service
5
+ to register, execute, and manage algorithms defined in Python. Algorithms can have dependencies
6
+ which are managed by Orca-core.
7
+ """
8
+
9
+ import re
10
+ import sys
11
+ import asyncio
12
+ import logging
13
+ import traceback
14
+
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
18
+ handlers=[logging.StreamHandler()],
19
+ )
20
+
21
+ import time
22
+ from typing import (
23
+ Any,
24
+ Dict,
25
+ List,
26
+ TypeVar,
27
+ Callable,
28
+ Iterable,
29
+ Generator,
30
+ TypeAlias,
31
+ AsyncGenerator,
32
+ )
33
+ from concurrent import futures
34
+ from dataclasses import dataclass
35
+
36
+ import grpc
37
+ import service_pb2 as pb
38
+ import service_pb2_grpc
39
+ import google.protobuf.struct_pb2 as struct_pb2
40
+ from google.protobuf import json_format
41
+ from service_pb2_grpc import OrcaProcessorServicer
42
+
43
+ from orca_python import envs
44
+ from orca_python.exceptions import InvalidDependency, InvalidAlgorithmArgument
45
+
46
+ # Regex patterns for validation
47
+ ALGORITHM_NAME = r"^[A-Z][a-zA-Z0-9]*$"
48
+ SEMVER_PATTERN = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$"
49
+ WINDOW_NAME = r"^[A-Z][a-zA-Z0-9]*$"
50
+
51
+ AlgorithmFn: TypeAlias = Callable[..., Any]
52
+
53
+ T = TypeVar("T", bound=AlgorithmFn)
54
+
55
+ LOGGER = logging.getLogger(__name__)
56
+
57
+
58
+ @dataclass
59
+ class Window:
60
+ time_from: int
61
+ time_to: int
62
+ name: str
63
+ version: str
64
+ origin: str
65
+
66
+
67
+ def EmitWindow(window: Window) -> None:
68
+ """
69
+ Emits a window to Orca-core.
70
+
71
+ Raises:
72
+ grpc.RpcError: If the emit fails.
73
+ """
74
+ LOGGER.info(f"Emitting window: {window}")
75
+
76
+ window_pb = pb.Window()
77
+ window_pb.time_to = window.time_to
78
+ window_pb.time_from = window.time_from
79
+ window_pb.window_type_name = window.name
80
+ window_pb.window_type_version = window.version
81
+ window_pb.origin = window.origin
82
+
83
+ with grpc.insecure_channel(envs.ORCASERVER) as channel:
84
+ stub = service_pb2_grpc.OrcaCoreStub(channel)
85
+ response = stub.EmitWindow(window_pb)
86
+ LOGGER.info(f"Window emitted: {response}")
87
+
88
+
89
+ @dataclass
90
+ class Algorithm:
91
+ """
92
+ Represents a registered algorithm with metadata and execution logic.
93
+
94
+ Attributes:
95
+ name (str): The name of the algorithm (PascalCase).
96
+ version (str): Semantic version of the algorithm (e.g., "1.0.0").
97
+ window_name (str): The window type name that triggers the algorithm.
98
+ window_version (str): The version of the window type.
99
+ exec_fn (AlgorithmFn): The execution function for the algorithm.
100
+ processor (str): Name of the processor where it's registered.
101
+ runtime (str): Python runtime used for execution.
102
+ """
103
+
104
+ name: str
105
+ version: str
106
+ window_name: str
107
+ window_version: str
108
+ exec_fn: AlgorithmFn
109
+ processor: str
110
+ runtime: str
111
+
112
+ @property
113
+ def full_name(self) -> str:
114
+ """Returns the full name as `name_version`."""
115
+ return f"{self.name}_{self.version}"
116
+
117
+ @property
118
+ def full_window_name(self) -> str:
119
+ """Returns the full window name as `window_name_window_version`."""
120
+ return f"{self.window_name}_{self.window_version}"
121
+
122
+
123
+ class Algorithms:
124
+ """
125
+ Internal singleton managing all registered algorithms and their dependencies.
126
+ """
127
+
128
+ def __init__(self) -> None:
129
+ self._flush()
130
+
131
+ def _flush(self) -> None:
132
+ """Clears all registered algorithms and dependencies."""
133
+ LOGGER.debug("Flushing all algorithm registrations and dependencies")
134
+ self._algorithms: Dict[str, Algorithm] = {}
135
+ self._dependencies: Dict[str, List[Algorithm]] = {}
136
+ self._dependencyFns: Dict[str, List[AlgorithmFn]] = {}
137
+ self._window_triggers: Dict[str, List[Algorithm]] = {}
138
+
139
+ def _add_algorithm(self, name: str, algorithm: Algorithm) -> None:
140
+ """
141
+ Registers a new algorithm.
142
+
143
+ Args:
144
+ name (str): Fully qualified algorithm name.
145
+ algorithm (Algorithm): Algorithm metadata and logic.
146
+
147
+ Raises:
148
+ ValueError: If the algorithm name is already registered.
149
+ """
150
+ if name in self._algorithms:
151
+ LOGGER.error(f"Attempted to register duplicate algorithm: {name}")
152
+ raise ValueError(f"Algorithm {name} already exists")
153
+ LOGGER.info(
154
+ f"Registering algorithm: {name} (window: {algorithm.window_name}_{algorithm.window_version})"
155
+ )
156
+ self._algorithms[name] = algorithm
157
+
158
+ def _add_dependency(self, algorithm: str, dependency: AlgorithmFn) -> None:
159
+ """
160
+ Adds a dependency to an algorithm.
161
+
162
+ Args:
163
+ algorithm (str): Target algorithm's full name.
164
+ dependency (AlgorithmFn): Dependency function already registered.
165
+
166
+ Raises:
167
+ ValueError: If the dependency function is not registered.
168
+ """
169
+ LOGGER.debug(f"Adding dependency for algorithm: {algorithm}")
170
+ dependencyAlgo = None
171
+ for algo in self._algorithms.values():
172
+ if algo.exec_fn == dependency:
173
+ dependencyAlgo = algo
174
+ break
175
+
176
+ if not dependencyAlgo:
177
+ LOGGER.error(
178
+ f"Failed to find registered algorithm for dependency: {dependency.__name__}"
179
+ )
180
+ raise ValueError(
181
+ f"Dependency {dependency.__name__} not found in registered algorithms"
182
+ )
183
+
184
+ if algorithm not in self._dependencyFns:
185
+ self._dependencyFns[algorithm] = [dependency]
186
+ self._dependencies[algorithm] = [dependencyAlgo]
187
+ else:
188
+ self._dependencyFns[algorithm].append(dependency)
189
+ self._dependencies[algorithm].append(dependencyAlgo)
190
+
191
+ def _add_window_trigger(self, window: str, algorithm: Algorithm) -> None:
192
+ """Associates an algorithm with a triggering window."""
193
+ if window not in self._window_triggers:
194
+ self._window_triggers[window] = [algorithm]
195
+ else:
196
+ self._window_triggers[window].append(algorithm)
197
+
198
+ def _has_algorithm_fn(self, algorithm_fn: AlgorithmFn) -> bool:
199
+ """
200
+ Checks if a function is registered as an algorithm.
201
+
202
+ Args:
203
+ algorithm_fn (AlgorithmFn): The function to check.
204
+
205
+ Returns:
206
+ bool: True if the function is registered.
207
+ """
208
+ for algorithm in self._algorithms.values():
209
+ if algorithm.exec_fn == algorithm_fn:
210
+ return True
211
+ return False
212
+
213
+
214
+ # the orca processor
215
+ class Processor(OrcaProcessorServicer): # type: ignore
216
+ """
217
+ Orca gRPC Processor for algorithm registration and execution.
218
+
219
+ This class implements the gRPC `OrcaProcessor` interface and handles
220
+ the execution lifecycle of user-defined algorithms.
221
+
222
+ Args:
223
+ name (str): Unique name of the processor.
224
+ max_workers (int): Max worker threads for execution (default: 10).
225
+ """
226
+
227
+ def __init__(self, name: str, max_workers: int = 10):
228
+ super().__init__()
229
+ self._name = name
230
+ self._processorConnStr = (
231
+ f"0.0.0.0:{envs.PORT}" # attach the processor to all network interfaces.
232
+ )
233
+ self._orcaProcessorConnStr = f"{envs.HOST}:{envs.PORT}" # tell orca-core to reference this processor by this address.
234
+ self._runtime = sys.version
235
+ self._max_workers = max_workers
236
+ self._algorithmsSingleton: Algorithms = Algorithms()
237
+
238
+ async def execute_algorithm(
239
+ self,
240
+ exec_id: str,
241
+ algorithm: pb.Algorithm,
242
+ dependencyResults: Iterable[pb.AlgorithmResult],
243
+ ) -> pb.ExecutionResult:
244
+ """
245
+ Executes a single algorithm with resolved dependencies.
246
+
247
+ Args:
248
+ exec_id (str): Unique execution ID.
249
+ algorithm (pb.Algorithm): The algorithm to execute.
250
+ dependencyResults (Iterable[pb.AlgorithmResult]): Results from dependency algorithms.
251
+
252
+ Returns:
253
+ pb.ExecutionResult: The result of the execution.
254
+
255
+ Raises:
256
+ Exception: On algorithm execution or serialization error.
257
+ """
258
+ try:
259
+ LOGGER.debug(f"Processing algorithm: {algorithm.name}_{algorithm.version}")
260
+ algoName = f"{algorithm.name}_{algorithm.version}"
261
+ algo = self._algorithmsSingleton._algorithms[algoName]
262
+
263
+ # convert dependency results into a dict of name -> value
264
+ dependency_values = {}
265
+ for dep_result in dependencyResults:
266
+ # extract value based on which oneof field is set
267
+ dep_value = None
268
+ if dep_result.result.HasField("single_value"):
269
+ dep_value = dep_result.result.single_value
270
+ elif dep_result.result.HasField("float_values"):
271
+ dep_value = list(dep_result.result.float_values.values)
272
+ elif dep_result.result.HasField("struct_value"):
273
+ dep_value = json_format.MessageToDict(
274
+ dep_result.result.struct_value
275
+ )
276
+
277
+ dep_name = f"{dep_result.algorithm.name}_{dep_result.algorithm.version}"
278
+ dependency_values[dep_name] = dep_value
279
+
280
+ # execute in thread pool since algo.exec_fn is synchronous
281
+ loop = asyncio.get_event_loop()
282
+
283
+ algoResult = await loop.run_in_executor(
284
+ None, algo.exec_fn, dependency_values
285
+ )
286
+
287
+ # create result based on the return type
288
+ current_time = int(time.time()) # Current timestamp in seconds
289
+
290
+ if isinstance(algoResult, dict):
291
+ # For dictionary results, use struct_value
292
+ struct_value = struct_pb2.Struct()
293
+ json_format.ParseDict(algoResult, struct_value)
294
+
295
+ resultPb = pb.Result(
296
+ status=pb.ResultStatus.RESULT_STATUS_SUCEEDED, # Note: Using the actual enum from the proto
297
+ struct_value=struct_value,
298
+ timestamp=current_time,
299
+ )
300
+ elif isinstance(algoResult, float) or isinstance(algoResult, int):
301
+ # for single numeric values
302
+ resultPb = pb.Result(
303
+ status=pb.ResultStatus.RESULT_STATUS_SUCEEDED,
304
+ single_value=float(algoResult), # Convert to float as per proto
305
+ timestamp=current_time,
306
+ )
307
+ elif isinstance(algoResult, list) and all(
308
+ isinstance(x, (int, float)) for x in algoResult
309
+ ):
310
+ # for lists of numeric values
311
+ float_array = pb.FloatArray(values=algoResult)
312
+ resultPb = pb.Result(
313
+ status=pb.ResultStatus.RESULT_STATUS_SUCEEDED,
314
+ float_values=float_array,
315
+ timestamp=current_time,
316
+ )
317
+ else:
318
+ # try to convert to struct as a fallback
319
+ try:
320
+ struct_value = struct_pb2.Struct()
321
+ # convert to dict if possible, otherwise use string representation
322
+ if hasattr(algoResult, "__dict__"):
323
+ result_dict = algoResult.__dict__
324
+ else:
325
+ result_dict = {"value": str(algoResult)}
326
+
327
+ json_format.ParseDict(result_dict, struct_value)
328
+ resultPb = pb.Result(
329
+ status=pb.ResultStatus.RESULT_STATUS_SUCEEDED,
330
+ struct_value=struct_value,
331
+ timestamp=current_time,
332
+ )
333
+ except Exception as conv_error:
334
+ LOGGER.error(
335
+ f"Failed to convert result to protobuf: {str(conv_error)}"
336
+ )
337
+ # create a handled failure result
338
+ resultPb = pb.Result(
339
+ status=pb.ResultStatus.RESULT_STATUS_HANDLED_FAILED,
340
+ timestamp=current_time,
341
+ )
342
+
343
+ # create the algorithm result
344
+ algoResultPb = pb.AlgorithmResult(
345
+ algorithm=algorithm, # Use the original algorithm object
346
+ result=resultPb,
347
+ )
348
+
349
+ # create the execution result
350
+ exec_result = pb.ExecutionResult(
351
+ exec_id=exec_id, algorithm_result=algoResultPb
352
+ )
353
+
354
+ LOGGER.info(f"Completed algorithm: {algorithm.name}")
355
+ return exec_result
356
+
357
+ except Exception as algo_error:
358
+ LOGGER.error(
359
+ f"Algorithm {algorithm.name} failed: {str(algo_error)}",
360
+ exc_info=True,
361
+ )
362
+
363
+ # create a failure result
364
+ current_time = int(time.time())
365
+
366
+ # create an error struct value with details
367
+ error_struct = struct_pb2.Struct()
368
+ json_format.ParseDict(
369
+ {"error": str(algo_error), "stack_trace": traceback.format_exc()},
370
+ error_struct,
371
+ )
372
+
373
+ # create the result with unhandled failed status and error info
374
+ error_result = pb.Result(
375
+ status=pb.ResultStatus.RESULT_STATUS_UNHANDLED_FAILED,
376
+ struct_value=error_struct,
377
+ timestamp=current_time,
378
+ )
379
+
380
+ # create the algorithm result
381
+ algo_result = pb.AlgorithmResult(algorithm=algorithm, result=error_result)
382
+
383
+ # create the execution result
384
+ return pb.ExecutionResult(exec_id=exec_id, algorithm_result=algo_result)
385
+
386
+ def ExecuteDagPart(
387
+ self, ExecutionRequest: pb.ExecutionRequest, context: grpc.ServicerContext
388
+ ) -> Generator[pb.ExecutionResult, None, None]:
389
+ """
390
+ Executes part of a DAG (Directed Acyclic Graph) of algorithms.
391
+
392
+ Args:
393
+ ExecutionRequest (pb.ExecutionRequest): The DAG execution request.
394
+ context (grpc.ServicerContext): gRPC context for the request.
395
+
396
+ Yields:
397
+ pb.ExecutionResult: Execution results streamed as they complete.
398
+
399
+ Raises:
400
+ grpc.RpcError: If execution fails and an internal error must be raised.
401
+ """
402
+
403
+ LOGGER.info(
404
+ (
405
+ f"Received DAG execution request with {len(ExecutionRequest.algorithms)} "
406
+ f"algorithms and ExecId: {ExecutionRequest.exec_id}"
407
+ )
408
+ )
409
+
410
+ try:
411
+ # create an event loop if it doesn't exist
412
+ try:
413
+ loop = asyncio.get_event_loop()
414
+ except RuntimeError:
415
+ loop = asyncio.new_event_loop()
416
+ asyncio.set_event_loop(loop)
417
+
418
+ # create tasks for all algorithms
419
+ tasks = [
420
+ self.execute_algorithm(
421
+ ExecutionRequest.exec_id,
422
+ algorithm,
423
+ ExecutionRequest.algorithm_results,
424
+ )
425
+ for algorithm in ExecutionRequest.algorithms
426
+ ]
427
+
428
+ # execute all tasks concurrently and yield results as they complete
429
+ async def process_results() -> AsyncGenerator[pb.ExecutionResult, None]:
430
+ for completed_task in asyncio.as_completed(tasks):
431
+ result = await completed_task
432
+ yield result
433
+
434
+ # run async generator in the event loop
435
+ async_gen = process_results()
436
+ while True:
437
+ try:
438
+ result = loop.run_until_complete(async_gen.__anext__())
439
+ yield result
440
+ except StopAsyncIteration:
441
+ break
442
+
443
+ # capture exceptions
444
+ except Exception as e:
445
+ LOGGER.error(f"DAG execution failed: {str(e)}", exc_info=True)
446
+ context.set_code(grpc.StatusCode.INTERNAL)
447
+ context.set_details(f"DAG execution failed: {str(e)}")
448
+ raise
449
+
450
+ except Exception as e:
451
+ LOGGER.error(f"DAG execution failed: {str(e)}", exc_info=True)
452
+ context.set_code(grpc.StatusCode.INTERNAL)
453
+ context.set_details(f"DAG execution failed: {str(e)}")
454
+ raise
455
+
456
+ def HealthCheck(
457
+ self, HealthCheckRequest: pb.HealthCheckRequest, context: grpc.ServicerContext
458
+ ) -> pb.HealthCheckResponse:
459
+ """
460
+ Returns health status for the processor.
461
+
462
+ Args:
463
+ HealthCheckRequest (pb.HealthCheckRequest): Incoming request.
464
+ context (grpc.ServicerContext): gRPC context.
465
+
466
+ Returns:
467
+ pb.HealthCheckResponse: Health status and optional metrics.
468
+ """
469
+
470
+ LOGGER.debug("Received health check request")
471
+ return pb.HealthCheckResponse(
472
+ status=pb.HealthCheckResponse.STATUS_SERVING,
473
+ message="Processor is healthy",
474
+ metrics=pb.ProcessorMetrics(
475
+ active_tasks=0, memory_bytes=0, cpu_percent=0.0, uptime_seconds=0
476
+ ),
477
+ )
478
+
479
+ def Register(self) -> None:
480
+ """
481
+ Registers all supported algorithms with the Orca Core service.
482
+
483
+ Raises:
484
+ grpc.RpcError: If registration fails.
485
+ """
486
+ LOGGER.info(f"Preparing to register processor '{self._name}' with Orca Core")
487
+ LOGGER.debug(
488
+ f"Building registration request with {len(self._algorithmsSingleton._algorithms)} algorithms"
489
+ )
490
+ registration_request = pb.ProcessorRegistration()
491
+ registration_request.name = self._name
492
+ registration_request.runtime = self._runtime
493
+ registration_request.connection_str = self._orcaProcessorConnStr
494
+
495
+ for _, algorithm in self._algorithmsSingleton._algorithms.items():
496
+ LOGGER.debug(
497
+ f"Adding algorithm to registration: {algorithm.name}_{algorithm.version}"
498
+ )
499
+ algo_msg = registration_request.supported_algorithms.add()
500
+ algo_msg.name = algorithm.name
501
+ algo_msg.version = algorithm.version
502
+
503
+ # Add window type
504
+ algo_msg.window_type.name = algorithm.window_name
505
+ algo_msg.window_type.version = algorithm.window_version
506
+
507
+ # Add dependencies if they exist
508
+ if algorithm.full_name in self._algorithmsSingleton._dependencies:
509
+ for dep in self._algorithmsSingleton._dependencies[algorithm.full_name]:
510
+ dep_msg = algo_msg.dependencies.add()
511
+ dep_msg.name = dep.name
512
+ dep_msg.version = dep.version
513
+ dep_msg.processor_name = dep.processor
514
+ dep_msg.processor_runtime = dep.runtime
515
+
516
+ with grpc.insecure_channel(envs.ORCASERVER) as channel:
517
+ stub = service_pb2_grpc.OrcaCoreStub(channel)
518
+ response = stub.RegisterProcessor(registration_request)
519
+ LOGGER.info(f"Algorithm registration response recieved: {response}")
520
+
521
+ def Start(self) -> None:
522
+ """
523
+ Starts the gRPC server and begins serving algorithm requests.
524
+
525
+ This includes signal handling for graceful shutdown.
526
+
527
+ Raises:
528
+ Exception: On server startup failure.
529
+ """
530
+ try:
531
+ LOGGER.info(
532
+ f"Starting Orca Processor '{self._name}' with Python {self._runtime}"
533
+ )
534
+ LOGGER.info(f"Initialising gRPC server with {self._max_workers} workers")
535
+
536
+ server = grpc.server(
537
+ futures.ThreadPoolExecutor(max_workers=self._max_workers),
538
+ options=[
539
+ ("grpc.max_send_message_length", 50 * 1024 * 1024), # 50MB
540
+ ("grpc.max_receive_message_length", 50 * 1024 * 1024), # 50MB
541
+ ],
542
+ )
543
+
544
+ # add our servicer to the server
545
+ service_pb2_grpc.add_OrcaProcessorServicer_to_server(self, server)
546
+
547
+ # add the server port
548
+ port = server.add_insecure_port(self._processorConnStr)
549
+ if port == 0:
550
+ raise RuntimeError(f"Failed to bind to port {envs.PORT}")
551
+
552
+ LOGGER.info(f"Server listening on address {self._processorConnStr}")
553
+
554
+ # start the server
555
+ server.start()
556
+ LOGGER.info("Server started successfully")
557
+
558
+ # setup graceful shutdown
559
+ import signal
560
+
561
+ def handle_shutdown(signum: int, frame: Any) -> None:
562
+ LOGGER.info("Received shutdown signal, stopping server...")
563
+ server.stop(grace=5) # 5 seconds grace period
564
+
565
+ signal.signal(signal.SIGTERM, handle_shutdown)
566
+ signal.signal(signal.SIGINT, handle_shutdown)
567
+
568
+ # wait for termination
569
+ LOGGER.info("Server is ready for requests")
570
+ server.wait_for_termination()
571
+
572
+ except Exception as e:
573
+ LOGGER.error(f"Failed to start server: {str(e)}", exc_info=True)
574
+ raise
575
+ finally:
576
+ LOGGER.info("Server shutdown complete")
577
+
578
+ def algorithm(
579
+ self,
580
+ name: str,
581
+ version: str,
582
+ window_name: str,
583
+ window_version: str,
584
+ depends_on: List[Callable[..., Any]] = [],
585
+ ) -> Callable[[T], T]:
586
+ """
587
+ Decorator for registering a function as an Orca algorithm.
588
+
589
+ Args:
590
+ name (str): Algorithm name (PascalCase).
591
+ version (str): Semantic version (e.g., "1.0.0").
592
+ window_name (str): Triggering window name (PascalCase).
593
+ window_version (str): Semantic version of the window.
594
+ depends_on (List[Callable]): List of dependent algorithm functions.
595
+
596
+ Returns:
597
+ Callable[[T], T]: The decorated function.
598
+
599
+ Raises:
600
+ InvalidAlgorithmArgument: If naming or version format is incorrect.
601
+ InvalidDependency: If any dependency is unregistered.
602
+ """
603
+ if not re.match(ALGORITHM_NAME, name):
604
+ raise InvalidAlgorithmArgument(
605
+ f"Algorithm name '{name}' must be in PascalCase"
606
+ )
607
+
608
+ if not re.match(SEMVER_PATTERN, version):
609
+ raise InvalidAlgorithmArgument(
610
+ f"Version '{version}' must follow basic semantic "
611
+ "versioning (e.g., '1.0.0') without release portions"
612
+ )
613
+
614
+ if not re.match(WINDOW_NAME, window_name):
615
+ raise InvalidAlgorithmArgument(
616
+ f"Window name '{window_name}' must be in PascalCase"
617
+ )
618
+
619
+ if not re.match(SEMVER_PATTERN, window_version):
620
+ raise InvalidAlgorithmArgument(
621
+ f"Window version '{window_version}' must follow basic semantic "
622
+ "versioning (e.g., '1.0.0') without release portions"
623
+ )
624
+
625
+ def inner(algo: T) -> T:
626
+ def wrapper(
627
+ dependency_values: Dict[str, Any] | None = None,
628
+ *args: Any,
629
+ **kwargs: Any,
630
+ ) -> Any:
631
+ LOGGER.debug(f"Executing algorithm {name}_{version}")
632
+ try:
633
+ # setup ready for the algo
634
+ # add dependency values to kwargs if provided
635
+ if dependency_values:
636
+ kwargs["dependencies"] = dependency_values
637
+ LOGGER.debug(f"Algorithm {name}_{version} setup complete")
638
+ # TODO
639
+
640
+ # run the algo
641
+ LOGGER.info(f"Running algorithm {name}_{version}")
642
+ result = algo(*args, **kwargs)
643
+ LOGGER.debug(f"Algorithm {name}_{version} execution complete")
644
+
645
+ # tear down
646
+ # TODO
647
+ return result
648
+ except Exception as e:
649
+ LOGGER.error(
650
+ f"Algorithm {name}_{version} failed: {str(e)}", exc_info=True
651
+ )
652
+ raise
653
+
654
+ algorithm = Algorithm(
655
+ name=name,
656
+ version=version,
657
+ window_name=window_name,
658
+ window_version=window_version,
659
+ exec_fn=wrapper,
660
+ processor=self._name,
661
+ runtime=sys.version,
662
+ )
663
+
664
+ self._algorithmsSingleton._add_algorithm(algorithm.full_name, algorithm)
665
+ self._algorithmsSingleton._add_window_trigger(
666
+ algorithm.full_window_name, algorithm
667
+ )
668
+
669
+ for dependency in depends_on:
670
+ if not self._algorithmsSingleton._has_algorithm_fn(dependency):
671
+ message = (
672
+ f"Cannot add function `{dependency.__name__}` to dependency stack. All dependencies must "
673
+ "be decorated with `@algorithm` before they can be used as dependencies."
674
+ )
675
+ raise InvalidDependency(message)
676
+ self._algorithmsSingleton._add_dependency(
677
+ algorithm.full_name, dependency
678
+ )
679
+
680
+ # TODO: check for circular dependencies. It's not easy to create one in python as the function
681
+ # needs to be defined before a dependency can be created, and you can only register depencenies
682
+ # once. But when dependencies are grabbed from a server, circular dependencies will be possible
683
+
684
+ return wrapper # type: ignore
685
+
686
+ return inner
orca_python/py.typed ADDED
File without changes
@@ -0,0 +1,16 @@
1
+ Copyright 2025 Predixus Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4
+ associated documentation files (the “Software”), to deal in the Software without restriction,
5
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
6
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7
+ furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or
10
+ substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
14
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
15
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
16
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.