cocoindex 0.2.3__cp311-abi3-win_amd64.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.
cocoindex/op.py ADDED
@@ -0,0 +1,628 @@
1
+ """
2
+ Facilities for defining cocoindex operations.
3
+ """
4
+
5
+ import asyncio
6
+ import dataclasses
7
+ import inspect
8
+ from enum import Enum
9
+ from typing import (
10
+ Any,
11
+ Awaitable,
12
+ Callable,
13
+ Protocol,
14
+ dataclass_transform,
15
+ Annotated,
16
+ get_args,
17
+ )
18
+
19
+ from . import _engine # type: ignore
20
+ from .subprocess_exec import executor_stub
21
+ from .convert import (
22
+ make_engine_value_encoder,
23
+ make_engine_value_decoder,
24
+ make_engine_key_decoder,
25
+ make_engine_struct_decoder,
26
+ )
27
+ from .typing import (
28
+ TypeAttr,
29
+ encode_enriched_type_info,
30
+ resolve_forward_ref,
31
+ analyze_type_info,
32
+ AnalyzedAnyType,
33
+ AnalyzedDictType,
34
+ )
35
+
36
+
37
+ class OpCategory(Enum):
38
+ """The category of the operation."""
39
+
40
+ FUNCTION = "function"
41
+ SOURCE = "source"
42
+ TARGET = "target"
43
+ DECLARATION = "declaration"
44
+
45
+
46
+ @dataclass_transform()
47
+ class SpecMeta(type):
48
+ """Meta class for spec classes."""
49
+
50
+ def __new__(
51
+ mcs,
52
+ name: str,
53
+ bases: tuple[type, ...],
54
+ attrs: dict[str, Any],
55
+ category: OpCategory | None = None,
56
+ ) -> type:
57
+ cls: type = super().__new__(mcs, name, bases, attrs)
58
+ if category is not None:
59
+ # It's the base class.
60
+ setattr(cls, "_op_category", category)
61
+ else:
62
+ # It's the specific class providing specific fields.
63
+ cls = dataclasses.dataclass(cls)
64
+ return cls
65
+
66
+
67
+ class SourceSpec(metaclass=SpecMeta, category=OpCategory.SOURCE): # pylint: disable=too-few-public-methods
68
+ """A source spec. All its subclass can be instantiated similar to a dataclass, i.e. ClassName(field1=value1, field2=value2, ...)"""
69
+
70
+
71
+ class FunctionSpec(metaclass=SpecMeta, category=OpCategory.FUNCTION): # pylint: disable=too-few-public-methods
72
+ """A function spec. All its subclass can be instantiated similar to a dataclass, i.e. ClassName(field1=value1, field2=value2, ...)"""
73
+
74
+
75
+ class TargetSpec(metaclass=SpecMeta, category=OpCategory.TARGET): # pylint: disable=too-few-public-methods
76
+ """A target spec. All its subclass can be instantiated similar to a dataclass, i.e. ClassName(field1=value1, field2=value2, ...)"""
77
+
78
+
79
+ class DeclarationSpec(metaclass=SpecMeta, category=OpCategory.DECLARATION): # pylint: disable=too-few-public-methods
80
+ """A declaration spec. All its subclass can be instantiated similar to a dataclass, i.e. ClassName(field1=value1, field2=value2, ...)"""
81
+
82
+
83
+ class Executor(Protocol):
84
+ """An executor for an operation."""
85
+
86
+ op_category: OpCategory
87
+
88
+
89
+ def _load_spec_from_engine(
90
+ spec_loader: Callable[..., Any], spec: dict[str, Any]
91
+ ) -> Any:
92
+ """
93
+ Load a spec from the engine.
94
+ """
95
+ return spec_loader(**spec)
96
+
97
+
98
+ def _get_required_method(cls: type, name: str) -> Callable[..., Any]:
99
+ method = getattr(cls, name, None)
100
+ if method is None:
101
+ raise ValueError(f"Method {name}() is required for {cls.__name__}")
102
+ if not inspect.isfunction(method):
103
+ raise ValueError(f"Method {cls.__name__}.{name}() is not a function")
104
+ return method
105
+
106
+
107
+ class _EngineFunctionExecutorFactory:
108
+ _spec_loader: Callable[..., Any]
109
+ _executor_cls: type
110
+
111
+ def __init__(self, spec_loader: Callable[..., Any], executor_cls: type):
112
+ self._spec_loader = spec_loader
113
+ self._executor_cls = executor_cls
114
+
115
+ def __call__(
116
+ self, spec: dict[str, Any], *args: Any, **kwargs: Any
117
+ ) -> tuple[dict[str, Any], Executor]:
118
+ spec = _load_spec_from_engine(self._spec_loader, spec)
119
+ executor = self._executor_cls(spec)
120
+ result_type = executor.analyze_schema(*args, **kwargs)
121
+ return (result_type, executor)
122
+
123
+
124
+ _COCOINDEX_ATTR_PREFIX = "cocoindex.io/"
125
+
126
+
127
+ class ArgRelationship(Enum):
128
+ """Specifies the relationship between an input argument and the output."""
129
+
130
+ EMBEDDING_ORIGIN_TEXT = _COCOINDEX_ATTR_PREFIX + "embedding_origin_text"
131
+ CHUNKS_BASE_TEXT = _COCOINDEX_ATTR_PREFIX + "chunk_base_text"
132
+ RECTS_BASE_IMAGE = _COCOINDEX_ATTR_PREFIX + "rects_base_image"
133
+
134
+
135
+ @dataclasses.dataclass
136
+ class OpArgs:
137
+ """
138
+ - gpu: Whether the executor will be executed on GPU.
139
+ - cache: Whether the executor will be cached.
140
+ - behavior_version: The behavior version of the executor. Cache will be invalidated if it
141
+ changes. Must be provided if `cache` is True.
142
+ - arg_relationship: It specifies the relationship between an input argument and the output,
143
+ e.g. `(ArgRelationship.CHUNKS_BASE_TEXT, "content")` means the output is chunks for the
144
+ input argument with name `content`.
145
+ """
146
+
147
+ gpu: bool = False
148
+ cache: bool = False
149
+ behavior_version: int | None = None
150
+ arg_relationship: tuple[ArgRelationship, str] | None = None
151
+
152
+
153
+ def _to_async_call(call: Callable[..., Any]) -> Callable[..., Awaitable[Any]]:
154
+ if inspect.iscoroutinefunction(call):
155
+ return call
156
+ return lambda *args, **kwargs: asyncio.to_thread(lambda: call(*args, **kwargs))
157
+
158
+
159
+ @dataclasses.dataclass
160
+ class _ArgInfo:
161
+ decoder: Callable[[Any], Any]
162
+ is_required: bool
163
+
164
+
165
+ def _register_op_factory(
166
+ category: OpCategory,
167
+ expected_args: list[tuple[str, inspect.Parameter]],
168
+ expected_return: Any,
169
+ executor_factory: Any,
170
+ spec_loader: Callable[..., Any],
171
+ op_kind: str,
172
+ op_args: OpArgs,
173
+ ) -> None:
174
+ """
175
+ Register an op factory.
176
+ """
177
+
178
+ class _WrappedExecutor:
179
+ _executor: Any
180
+ _args_info: list[_ArgInfo]
181
+ _kwargs_info: dict[str, _ArgInfo]
182
+ _result_encoder: Callable[[Any], Any]
183
+ _acall: Callable[..., Awaitable[Any]] | None = None
184
+
185
+ def __init__(self, spec: Any) -> None:
186
+ executor: Any
187
+
188
+ if op_args.gpu:
189
+ executor = executor_stub(executor_factory, spec)
190
+ else:
191
+ executor = executor_factory()
192
+ executor.spec = spec
193
+
194
+ self._executor = executor
195
+
196
+ def analyze_schema(
197
+ self, *args: _engine.OpArgSchema, **kwargs: _engine.OpArgSchema
198
+ ) -> Any:
199
+ """
200
+ Analyze the spec and arguments. In this phase, argument types should be validated.
201
+ It should return the expected result type for the current op.
202
+ """
203
+ self._args_info = []
204
+ self._kwargs_info = {}
205
+ attributes = []
206
+ potentially_missing_required_arg = False
207
+
208
+ def process_arg(
209
+ arg_name: str,
210
+ arg_param: inspect.Parameter,
211
+ actual_arg: _engine.OpArgSchema,
212
+ ) -> _ArgInfo:
213
+ nonlocal potentially_missing_required_arg
214
+ if op_args.arg_relationship is not None:
215
+ related_attr, related_arg_name = op_args.arg_relationship
216
+ if related_arg_name == arg_name:
217
+ attributes.append(
218
+ TypeAttr(related_attr.value, actual_arg.analyzed_value)
219
+ )
220
+ type_info = analyze_type_info(arg_param.annotation)
221
+ decoder = make_engine_value_decoder(
222
+ [arg_name], actual_arg.value_type["type"], type_info
223
+ )
224
+ is_required = not type_info.nullable
225
+ if is_required and actual_arg.value_type.get("nullable", False):
226
+ potentially_missing_required_arg = True
227
+ return _ArgInfo(
228
+ decoder=decoder,
229
+ is_required=is_required,
230
+ )
231
+
232
+ # Match arguments with parameters.
233
+ next_param_idx = 0
234
+ for actual_arg in args:
235
+ if next_param_idx >= len(expected_args):
236
+ raise ValueError(
237
+ f"Too many arguments passed in: {len(args)} > {len(expected_args)}"
238
+ )
239
+ arg_name, arg_param = expected_args[next_param_idx]
240
+ if arg_param.kind in (
241
+ inspect.Parameter.KEYWORD_ONLY,
242
+ inspect.Parameter.VAR_KEYWORD,
243
+ ):
244
+ raise ValueError(
245
+ f"Too many positional arguments passed in: {len(args)} > {next_param_idx}"
246
+ )
247
+ self._args_info.append(process_arg(arg_name, arg_param, actual_arg))
248
+ if arg_param.kind != inspect.Parameter.VAR_POSITIONAL:
249
+ next_param_idx += 1
250
+
251
+ expected_kwargs = expected_args[next_param_idx:]
252
+
253
+ for kwarg_name, actual_arg in kwargs.items():
254
+ expected_arg = next(
255
+ (
256
+ arg
257
+ for arg in expected_kwargs
258
+ if (
259
+ arg[0] == kwarg_name
260
+ and arg[1].kind
261
+ in (
262
+ inspect.Parameter.KEYWORD_ONLY,
263
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
264
+ )
265
+ )
266
+ or arg[1].kind == inspect.Parameter.VAR_KEYWORD
267
+ ),
268
+ None,
269
+ )
270
+ if expected_arg is None:
271
+ raise ValueError(
272
+ f"Unexpected keyword argument passed in: {kwarg_name}"
273
+ )
274
+ arg_param = expected_arg[1]
275
+ self._kwargs_info[kwarg_name] = process_arg(
276
+ kwarg_name, arg_param, actual_arg
277
+ )
278
+
279
+ missing_args = [
280
+ name
281
+ for (name, arg) in expected_kwargs
282
+ if arg.default is inspect.Parameter.empty
283
+ and (
284
+ arg.kind == inspect.Parameter.POSITIONAL_ONLY
285
+ or (
286
+ arg.kind
287
+ in (
288
+ inspect.Parameter.KEYWORD_ONLY,
289
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
290
+ )
291
+ and name not in kwargs
292
+ )
293
+ )
294
+ ]
295
+ if len(missing_args) > 0:
296
+ raise ValueError(f"Missing arguments: {', '.join(missing_args)}")
297
+
298
+ base_analyze_method = getattr(self._executor, "analyze", None)
299
+ if base_analyze_method is not None:
300
+ result_type = base_analyze_method()
301
+ else:
302
+ result_type = expected_return
303
+ if len(attributes) > 0:
304
+ result_type = Annotated[result_type, *attributes]
305
+
306
+ analyzed_result_type_info = analyze_type_info(result_type)
307
+ encoded_type = encode_enriched_type_info(analyzed_result_type_info)
308
+ if potentially_missing_required_arg:
309
+ encoded_type["nullable"] = True
310
+
311
+ self._result_encoder = make_engine_value_encoder(analyzed_result_type_info)
312
+
313
+ return encoded_type
314
+
315
+ async def prepare(self) -> None:
316
+ """
317
+ Prepare for execution.
318
+ It's executed after `analyze` and before any `__call__` execution.
319
+ """
320
+ prepare_method = getattr(self._executor, "prepare", None)
321
+ if prepare_method is not None:
322
+ await _to_async_call(prepare_method)()
323
+ self._acall = _to_async_call(self._executor.__call__)
324
+
325
+ async def __call__(self, *args: Any, **kwargs: Any) -> Any:
326
+ decoded_args = []
327
+ for arg_info, arg in zip(self._args_info, args):
328
+ if arg_info.is_required and arg is None:
329
+ return None
330
+ decoded_args.append(arg_info.decoder(arg))
331
+
332
+ decoded_kwargs = {}
333
+ for kwarg_name, arg in kwargs.items():
334
+ kwarg_info = self._kwargs_info.get(kwarg_name)
335
+ if kwarg_info is None:
336
+ raise ValueError(
337
+ f"Unexpected keyword argument passed in: {kwarg_name}"
338
+ )
339
+ if kwarg_info.is_required and arg is None:
340
+ return None
341
+ decoded_kwargs[kwarg_name] = kwarg_info.decoder(arg)
342
+
343
+ assert self._acall is not None
344
+ output = await self._acall(*decoded_args, **decoded_kwargs)
345
+ return self._result_encoder(output)
346
+
347
+ def enable_cache(self) -> bool:
348
+ return op_args.cache
349
+
350
+ def behavior_version(self) -> int | None:
351
+ return op_args.behavior_version
352
+
353
+ if category == OpCategory.FUNCTION:
354
+ _engine.register_function_factory(
355
+ op_kind, _EngineFunctionExecutorFactory(spec_loader, _WrappedExecutor)
356
+ )
357
+ else:
358
+ raise ValueError(f"Unsupported executor type {category}")
359
+
360
+
361
+ def executor_class(**args: Any) -> Callable[[type], type]:
362
+ """
363
+ Decorate a class to provide an executor for an op.
364
+ """
365
+ op_args = OpArgs(**args)
366
+
367
+ def _inner(cls: type[Executor]) -> type:
368
+ """
369
+ Decorate a class to provide an executor for an op.
370
+ """
371
+ # Use `__annotations__` instead of `get_type_hints`, to avoid resolving forward references.
372
+ type_hints = cls.__annotations__
373
+ if "spec" not in type_hints:
374
+ raise TypeError("Expect a `spec` field with type hint")
375
+ spec_cls = resolve_forward_ref(type_hints["spec"])
376
+ sig = inspect.signature(cls.__call__)
377
+ _register_op_factory(
378
+ category=spec_cls._op_category,
379
+ expected_args=list(sig.parameters.items())[1:], # First argument is `self`
380
+ expected_return=sig.return_annotation,
381
+ executor_factory=cls,
382
+ spec_loader=spec_cls,
383
+ op_kind=spec_cls.__name__,
384
+ op_args=op_args,
385
+ )
386
+ return cls
387
+
388
+ return _inner
389
+
390
+
391
+ class EmptyFunctionSpec(FunctionSpec):
392
+ pass
393
+
394
+
395
+ class _SimpleFunctionExecutor:
396
+ spec: Callable[..., Any]
397
+
398
+ def prepare(self) -> None:
399
+ self.__call__ = staticmethod(self.spec)
400
+
401
+
402
+ def function(**args: Any) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
403
+ """
404
+ Decorate a function to provide a function for an op.
405
+ """
406
+ op_args = OpArgs(**args)
407
+
408
+ def _inner(fn: Callable[..., Any]) -> Callable[..., Any]:
409
+ # Convert snake case to camel case.
410
+ op_kind = "".join(word.capitalize() for word in fn.__name__.split("_"))
411
+ sig = inspect.signature(fn)
412
+ fn.__cocoindex_op_kind__ = op_kind # type: ignore
413
+ _register_op_factory(
414
+ category=OpCategory.FUNCTION,
415
+ expected_args=list(sig.parameters.items()),
416
+ expected_return=sig.return_annotation,
417
+ executor_factory=_SimpleFunctionExecutor,
418
+ spec_loader=lambda: fn,
419
+ op_kind=op_kind,
420
+ op_args=op_args,
421
+ )
422
+
423
+ return fn
424
+
425
+ return _inner
426
+
427
+
428
+ ########################################################
429
+ # Custom target connector
430
+ ########################################################
431
+
432
+
433
+ @dataclasses.dataclass
434
+ class _TargetConnectorContext:
435
+ target_name: str
436
+ spec: Any
437
+ prepared_spec: Any
438
+ key_decoder: Callable[[Any], Any]
439
+ value_decoder: Callable[[Any], Any]
440
+
441
+
442
+ class _TargetConnector:
443
+ """
444
+ The connector class passed to the engine.
445
+ """
446
+
447
+ _spec_cls: type
448
+ _connector_cls: type
449
+
450
+ _get_persistent_key_fn: Callable[[_TargetConnectorContext, str], Any]
451
+ _apply_setup_change_async_fn: Callable[
452
+ [Any, dict[str, Any] | None, dict[str, Any] | None], Awaitable[None]
453
+ ]
454
+ _mutate_async_fn: Callable[..., Awaitable[None]]
455
+ _mutatation_type: AnalyzedDictType | None
456
+
457
+ def __init__(self, spec_cls: type, connector_cls: type):
458
+ self._spec_cls = spec_cls
459
+ self._connector_cls = connector_cls
460
+
461
+ self._get_persistent_key_fn = _get_required_method(
462
+ connector_cls, "get_persistent_key"
463
+ )
464
+ self._apply_setup_change_async_fn = _to_async_call(
465
+ _get_required_method(connector_cls, "apply_setup_change")
466
+ )
467
+
468
+ mutate_fn = _get_required_method(connector_cls, "mutate")
469
+ self._mutate_async_fn = _to_async_call(mutate_fn)
470
+
471
+ # Store the type annotation for later use
472
+ self._mutatation_type = self._analyze_mutate_mutation_type(
473
+ connector_cls, mutate_fn
474
+ )
475
+
476
+ @staticmethod
477
+ def _analyze_mutate_mutation_type(
478
+ connector_cls: type, mutate_fn: Callable[..., Any]
479
+ ) -> AnalyzedDictType | None:
480
+ # Validate mutate_fn signature and extract type annotation
481
+ mutate_sig = inspect.signature(mutate_fn)
482
+ params = list(mutate_sig.parameters.values())
483
+
484
+ if len(params) != 1:
485
+ raise ValueError(
486
+ f"Method {connector_cls.__name__}.mutate(*args) must have exactly one parameter, "
487
+ f"got {len(params)}"
488
+ )
489
+
490
+ param = params[0]
491
+ if param.kind != inspect.Parameter.VAR_POSITIONAL:
492
+ raise ValueError(
493
+ f"Method {connector_cls.__name__}.mutate(*args) parameter must be *args format, "
494
+ f"got {param.kind.name}"
495
+ )
496
+
497
+ # Extract type annotation
498
+ analyzed_args_type = analyze_type_info(param.annotation)
499
+ if isinstance(analyzed_args_type.variant, AnalyzedAnyType):
500
+ return None
501
+
502
+ if analyzed_args_type.base_type is tuple:
503
+ args = get_args(analyzed_args_type.core_type)
504
+ if not args:
505
+ return None
506
+ if len(args) == 2:
507
+ mutation_type = analyze_type_info(args[1])
508
+ if isinstance(mutation_type.variant, AnalyzedAnyType):
509
+ return None
510
+ if isinstance(mutation_type.variant, AnalyzedDictType):
511
+ return mutation_type.variant
512
+
513
+ raise ValueError(
514
+ f"Method {connector_cls.__name__}.mutate(*args) parameter must be a tuple with "
515
+ f"2 elements (tuple[SpecType, dict[str, ValueStruct]], spec and mutation in dict), "
516
+ "got {args_type}"
517
+ )
518
+
519
+ def create_export_context(
520
+ self,
521
+ name: str,
522
+ spec: dict[str, Any],
523
+ key_fields_schema: list[Any],
524
+ value_fields_schema: list[Any],
525
+ ) -> _TargetConnectorContext:
526
+ key_annotation, value_annotation = (
527
+ (
528
+ self._mutatation_type.key_type,
529
+ self._mutatation_type.value_type,
530
+ )
531
+ if self._mutatation_type is not None
532
+ else (Any, Any)
533
+ )
534
+
535
+ key_decoder = make_engine_key_decoder(
536
+ ["(key)"], key_fields_schema, analyze_type_info(key_annotation)
537
+ )
538
+ value_decoder = make_engine_struct_decoder(
539
+ ["(value)"], value_fields_schema, analyze_type_info(value_annotation)
540
+ )
541
+
542
+ loaded_spec = _load_spec_from_engine(self._spec_cls, spec)
543
+ prepare_method = getattr(self._connector_cls, "prepare", None)
544
+ if prepare_method is None:
545
+ prepared_spec = loaded_spec
546
+ else:
547
+ prepared_spec = prepare_method(loaded_spec)
548
+
549
+ return _TargetConnectorContext(
550
+ target_name=name,
551
+ spec=loaded_spec,
552
+ prepared_spec=prepared_spec,
553
+ key_decoder=key_decoder,
554
+ value_decoder=value_decoder,
555
+ )
556
+
557
+ def get_persistent_key(self, export_context: _TargetConnectorContext) -> Any:
558
+ return self._get_persistent_key_fn(
559
+ export_context.spec, export_context.target_name
560
+ )
561
+
562
+ def describe_resource(self, key: Any) -> str:
563
+ describe_fn = getattr(self._connector_cls, "describe", None)
564
+ if describe_fn is None:
565
+ return str(key)
566
+ return str(describe_fn(key))
567
+
568
+ async def apply_setup_changes_async(
569
+ self,
570
+ changes: list[tuple[Any, list[dict[str, Any] | None], dict[str, Any] | None]],
571
+ ) -> None:
572
+ for key, previous, current in changes:
573
+ prev_specs = [
574
+ _load_spec_from_engine(self._spec_cls, spec)
575
+ if spec is not None
576
+ else None
577
+ for spec in previous
578
+ ]
579
+ curr_spec = (
580
+ _load_spec_from_engine(self._spec_cls, current)
581
+ if current is not None
582
+ else None
583
+ )
584
+ for prev_spec in prev_specs:
585
+ await self._apply_setup_change_async_fn(key, prev_spec, curr_spec)
586
+
587
+ @staticmethod
588
+ def _decode_mutation(
589
+ context: _TargetConnectorContext, mutation: list[tuple[Any, Any | None]]
590
+ ) -> tuple[Any, dict[Any, Any | None]]:
591
+ return (
592
+ context.prepared_spec,
593
+ {
594
+ context.key_decoder(key): (
595
+ context.value_decoder(value) if value is not None else None
596
+ )
597
+ for key, value in mutation
598
+ },
599
+ )
600
+
601
+ async def mutate_async(
602
+ self,
603
+ mutations: list[tuple[_TargetConnectorContext, list[tuple[Any, Any | None]]]],
604
+ ) -> None:
605
+ await self._mutate_async_fn(
606
+ *(
607
+ self._decode_mutation(context, mutation)
608
+ for context, mutation in mutations
609
+ )
610
+ )
611
+
612
+
613
+ def target_connector(spec_cls: type) -> Callable[[type], type]:
614
+ """
615
+ Decorate a class to provide a target connector for an op.
616
+ """
617
+
618
+ # Validate the spec_cls is a TargetSpec.
619
+ if not issubclass(spec_cls, TargetSpec):
620
+ raise ValueError(f"Expect a TargetSpec, got {spec_cls}")
621
+
622
+ # Register the target connector.
623
+ def _inner(connector_cls: type) -> type:
624
+ connector = _TargetConnector(spec_cls, connector_cls)
625
+ _engine.register_target_connector(spec_cls.__name__, connector)
626
+ return connector_cls
627
+
628
+ return _inner
cocoindex/py.typed ADDED
File without changes
cocoindex/runtime.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ This module provides a standalone execution runtime for executing coroutines in a thread-safe
3
+ manner.
4
+ """
5
+
6
+ import threading
7
+ import asyncio
8
+ from typing import Any, Coroutine, TypeVar
9
+
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ class _ExecutionContext:
15
+ _lock: threading.Lock
16
+ _event_loop: asyncio.AbstractEventLoop | None = None
17
+
18
+ def __init__(self) -> None:
19
+ self._lock = threading.Lock()
20
+
21
+ @property
22
+ def event_loop(self) -> asyncio.AbstractEventLoop:
23
+ """Get the event loop for the cocoindex library."""
24
+ with self._lock:
25
+ if self._event_loop is None:
26
+ self._event_loop = asyncio.new_event_loop()
27
+ threading.Thread(
28
+ target=self._event_loop.run_forever, daemon=True
29
+ ).start()
30
+ return self._event_loop
31
+
32
+ def run(self, coro: Coroutine[Any, Any, T]) -> T:
33
+ """Run a coroutine in the event loop, blocking until it finishes. Return its result."""
34
+ return asyncio.run_coroutine_threadsafe(coro, self.event_loop).result()
35
+
36
+
37
+ execution_context = _ExecutionContext()