diff --git a/examples/fanout_sync/test_fanout_sync.py b/examples/fanout_sync/test_fanout_sync.py new file mode 100644 index 00000000..7c70bccc --- /dev/null +++ b/examples/fanout_sync/test_fanout_sync.py @@ -0,0 +1,10 @@ +import pytest + +from hatchet_sdk import Hatchet, Worker + + +@pytest.mark.parametrize("worker", ["fanout_sync"], indirect=True) +def test_run(hatchet: Hatchet, worker: Worker) -> None: + run = hatchet.admin.run_workflow("SyncFanoutParent", {"n": 2}) + result = run.sync_result() + assert len(result["spawn"]["results"]) == 2 diff --git a/examples/fanout_sync/trigger.py b/examples/fanout_sync/trigger.py new file mode 100644 index 00000000..d5ac99b8 --- /dev/null +++ b/examples/fanout_sync/trigger.py @@ -0,0 +1,20 @@ +import asyncio + +from dotenv import load_dotenv + +from hatchet_sdk import new_client + + +async def main() -> None: + load_dotenv() + hatchet = new_client() + + hatchet.admin.run_workflow( + "SyncFanoutParent", + {"test": "test"}, + options={"additional_metadata": {"hello": "moon"}}, + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/fanout_sync/worker.py b/examples/fanout_sync/worker.py new file mode 100644 index 00000000..a2020844 --- /dev/null +++ b/examples/fanout_sync/worker.py @@ -0,0 +1,55 @@ +from typing import Any + +from dotenv import load_dotenv + +from hatchet_sdk import Context, Hatchet +from hatchet_sdk.workflow_run import WorkflowRunRef + +load_dotenv() + +hatchet = Hatchet(debug=True) + + +@hatchet.workflow(on_events=["parent:create"]) +class SyncFanoutParent: + @hatchet.step(timeout="5m") + def spawn(self, context: Context) -> dict[str, Any]: + print("spawning child") + + n = context.workflow_input().get("n", 5) + + runs = context.spawn_workflows( + [ + { + "workflow_name": "SyncFanoutChild", + "input": {"a": str(i)}, + "key": f"child{i}", + "options": {"additional_metadata": {"hello": "earth"}}, + } + for i in range(n) + ] + ) + + results = [r.sync_result() for r in runs] + + print(f"results {results}") + + return {"results": results} + + +@hatchet.workflow(on_events=["child:create"]) +class SyncFanoutChild: + @hatchet.step() + def process(self, context: Context) -> dict[str, str]: + return {"status": "success " + context.workflow_input()["a"]} + + +def main() -> None: + worker = hatchet.worker("sync-fanout-worker", max_runs=40) + worker.register_workflow(SyncFanoutParent()) + worker.register_workflow(SyncFanoutChild()) + worker.start() + + +if __name__ == "__main__": + main() diff --git a/examples/opentelemetry_instrumentation/test_otel_instrumentation.py b/examples/opentelemetry_instrumentation/test_otel_instrumentation.py index fa4e785d..4ec3def2 100644 --- a/examples/opentelemetry_instrumentation/test_otel_instrumentation.py +++ b/examples/opentelemetry_instrumentation/test_otel_instrumentation.py @@ -6,7 +6,11 @@ from hatchet_sdk import Hatchet, Worker from hatchet_sdk.clients.admin import TriggerWorkflowOptions from hatchet_sdk.clients.events import PushEventOptions -from hatchet_sdk.opentelemetry.instrumentor import HatchetInstrumentor +from hatchet_sdk.opentelemetry.instrumentor import ( + HatchetInstrumentor, + create_traceparent, + inject_traceparent_into_metadata, +) trace_provider = NoOpTracerProvider() @@ -17,9 +21,7 @@ def create_additional_metadata() -> dict[str, str]: - return instrumentor.inject_traceparent_into_metadata( - {"hello": "world"}, instrumentor.create_traceparent() - ) + return inject_traceparent_into_metadata({"hello": "world"}) def create_push_options() -> PushEventOptions: diff --git a/examples/opentelemetry_instrumentation/triggers.py b/examples/opentelemetry_instrumentation/triggers.py index 3a7b8cb0..b9ab4fd6 100644 --- a/examples/opentelemetry_instrumentation/triggers.py +++ b/examples/opentelemetry_instrumentation/triggers.py @@ -4,16 +4,18 @@ from examples.opentelemetry_instrumentation.tracer import trace_provider from hatchet_sdk.clients.admin import TriggerWorkflowOptions from hatchet_sdk.clients.events import PushEventOptions -from hatchet_sdk.opentelemetry.instrumentor import HatchetInstrumentor +from hatchet_sdk.opentelemetry.instrumentor import ( + HatchetInstrumentor, + create_traceparent, + inject_traceparent_into_metadata, +) instrumentor = HatchetInstrumentor(tracer_provider=trace_provider) tracer = trace_provider.get_tracer(__name__) def create_additional_metadata() -> dict[str, str]: - return instrumentor.inject_traceparent_into_metadata( - {"hello": "world"}, instrumentor.create_traceparent() - ) + return inject_traceparent_into_metadata({"hello": "world"}) def create_push_options() -> PushEventOptions: diff --git a/hatchet_sdk/clients/workflow_listener.py b/hatchet_sdk/clients/workflow_listener.py index b1131587..8bf71a3c 100644 --- a/hatchet_sdk/clients/workflow_listener.py +++ b/hatchet_sdk/clients/workflow_listener.py @@ -75,6 +75,12 @@ class PooledWorkflowRunListener: interrupter: asyncio.Task = None def __init__(self, config: ClientConfig): + try: + asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + conn = new_conn(config, True) self.client = DispatcherStub(conn) self.token = config.token @@ -260,12 +266,10 @@ async def _retry_subscribe(self): if self.curr_requester != 0: self.requests.put_nowait(self.curr_requester) - listener = self.client.SubscribeToWorkflowRuns( + return self.client.SubscribeToWorkflowRuns( self._request(), metadata=get_metadata(self.token), ) - - return listener except grpc.RpcError as e: if e.code() == grpc.StatusCode.UNAVAILABLE: retries = retries + 1 diff --git a/hatchet_sdk/context/context.py b/hatchet_sdk/context/context.py index f20acd66..2584949b 100644 --- a/hatchet_sdk/context/context.py +++ b/hatchet_sdk/context/context.py @@ -403,3 +403,44 @@ def fetch_run_failures(self) -> list[dict[str, StrictStr]]: for step_run in job_run.step_runs if step_run.error and step_run.step ] + + @tenacity_retry + def spawn_workflow( + self, + workflow_name: str, + input: dict[str, Any] = {}, + key: str | None = None, + options: ChildTriggerWorkflowOptions | None = None, + ) -> WorkflowRunRef: + worker_id = self.worker.id() + trigger_options = self._prepare_workflow_options(key, options, worker_id) + + return self.admin_client.run_workflow(workflow_name, input, trigger_options) + + @tenacity_retry + def spawn_workflows( + self, child_workflow_runs: list[ChildWorkflowRunDict] + ) -> list[WorkflowRunRef]: + + if len(child_workflow_runs) == 0: + raise Exception("no child workflows to spawn") + + worker_id = self.worker.id() + + bulk_trigger_workflow_runs: list[WorkflowRunDict] = [] + for child_workflow_run in child_workflow_runs: + workflow_name = child_workflow_run["workflow_name"] + input = child_workflow_run["input"] + + key = child_workflow_run.get("key") + options = child_workflow_run.get("options", {}) + + trigger_options = self._prepare_workflow_options(key, options, worker_id) + + bulk_trigger_workflow_runs.append( + WorkflowRunDict( + workflow_name=workflow_name, input=input, options=trigger_options + ) + ) + + return self.admin_client.run_workflows(bulk_trigger_workflow_runs) diff --git a/hatchet_sdk/loader.py b/hatchet_sdk/loader.py index 6d8884d6..0252f33a 100644 --- a/hatchet_sdk/loader.py +++ b/hatchet_sdk/loader.py @@ -42,6 +42,7 @@ def __init__( worker_healthcheck_port: int | None = None, worker_healthcheck_enabled: bool | None = None, worker_preset_labels: dict[str, str] = {}, + enable_force_kill_sync_threads: bool = False, ): self.tenant_id = tenant_id self.tls_config = tls_config @@ -55,6 +56,7 @@ def __init__( self.worker_healthcheck_port = worker_healthcheck_port self.worker_healthcheck_enabled = worker_healthcheck_enabled self.worker_preset_labels = worker_preset_labels + self.enable_force_kill_sync_threads = enable_force_kill_sync_threads if not self.logInterceptor: self.logInterceptor = getLogger() @@ -174,6 +176,14 @@ def get_config_value(key, env_var): "The `otel_exporter_otlp_*` fields are no longer supported as of SDK version `0.46.0`. Please see the documentation on OpenTelemetry at https://docs.hatchet.run/home/features/opentelemetry for more information on how to migrate to the new `HatchetInstrumentor`." ) + enable_force_kill_sync_threads = bool( + get_config_value( + "enable_force_kill_sync_threads", + "HATCHET_CLIENT_ENABLE_FORCE_KILL_SYNC_THREADS", + ) + == "True" + or False + ) return ClientConfig( tenant_id=tenant_id, tls_config=tls_config, @@ -188,6 +198,7 @@ def get_config_value(key, env_var): worker_healthcheck_port=worker_healthcheck_port, worker_healthcheck_enabled=worker_healthcheck_enabled, worker_preset_labels=worker_preset_labels, + enable_force_kill_sync_threads=enable_force_kill_sync_threads, ) def _load_tls_config(self, tls_data: Dict, host_port) -> ClientTLSConfig: diff --git a/hatchet_sdk/opentelemetry/instrumentor.py b/hatchet_sdk/opentelemetry/instrumentor.py index d13e3fc5..91474c52 100644 --- a/hatchet_sdk/opentelemetry/instrumentor.py +++ b/hatchet_sdk/opentelemetry/instrumentor.py @@ -13,6 +13,7 @@ StatusCode, TracerProvider, get_tracer, + get_tracer_provider, ) from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, @@ -43,49 +44,121 @@ InstrumentKwargs = TracerProvider | MeterProvider | None +OTEL_TRACEPARENT_KEY = "traceparent" -class HatchetInstrumentor(BaseInstrumentor): # type: ignore[misc] - OTEL_TRACEPARENT_KEY = "traceparent" - def __init__( - self, - tracer_provider: TracerProvider, - meter_provider: MeterProvider = NoOpMeterProvider(), - ): - self.tracer_provider = tracer_provider - self.meter_provider = meter_provider +def create_traceparent() -> str | None: + """ + Creates and returns a W3C traceparent header value using OpenTelemetry's context propagation. - super().__init__() + The traceparent header is used to propagate context information across service boundaries + in distributed tracing systems. It follows the W3C Trace Context specification. - def create_traceparent(self) -> str | None: - carrier: dict[str, str] = {} - TraceContextTextMapPropagator().inject(carrier) + :returns: A W3C-formatted traceparent header value if successful, None if the context + injection fails or no active span exists.\n + Example: `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01` + :rtype: str | None: + """ - return carrier.get("traceparent") + carrier: dict[str, str] = {} + TraceContextTextMapPropagator().inject(carrier) - def parse_carrier_from_metadata( - self, metadata: dict[str, str] | None - ) -> Context | None: - if not metadata: - return None + return carrier.get("traceparent") - traceparent = metadata.get(self.OTEL_TRACEPARENT_KEY) - if not traceparent: - return None +def parse_carrier_from_metadata(metadata: dict[str, str] | None) -> Context | None: + """ + Parses OpenTelemetry trace context from a metadata dictionary. - return TraceContextTextMapPropagator().extract( - {self.OTEL_TRACEPARENT_KEY: traceparent} - ) + Extracts the trace context from metadata using the W3C Trace Context format, + specifically looking for the `traceparent` header. + + :param metadata: A dictionary containing metadata key-value pairs, + potentially including the `traceparent` header. Can be None. + :type metadata: dict[str, str] | None + :returns: The extracted OpenTelemetry Context object if a valid `traceparent` + is found in the metadata, otherwise None. + :rtype: Context | None + + :Example: + + >>> metadata = {"traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"} + >>> context = parse_carrier_from_metadata(metadata) + """ + + if not metadata: + return None + + traceparent = metadata.get(OTEL_TRACEPARENT_KEY) + + if not traceparent: + return None + + return TraceContextTextMapPropagator().extract({OTEL_TRACEPARENT_KEY: traceparent}) + + +def inject_traceparent_into_metadata( + metadata: dict[str, str], traceparent: str | None = None +) -> dict[str, str]: + """ + Injects OpenTelemetry `traceparent` into a metadata dictionary. + + Takes a metadata dictionary and an optional `traceparent` string, + returning a new metadata dictionary with the `traceparent` added under the + `OTEL_TRACEPARENT_KEY`. If no `traceparent` is provided, it attempts to create one. - def inject_traceparent_into_metadata( - self, metadata: dict[str, str], traceparent: str | None - ) -> dict[str, str]: - if traceparent: - metadata[self.OTEL_TRACEPARENT_KEY] = traceparent + :param metadata: The metadata dictionary to inject the `traceparent` into. + :type metadata: dict[str, str] + :param traceparent: The `traceparent` string to inject. If None, attempts to use + the current span. + :type traceparent: str | None, optional + :returns: A new metadata dictionary containing the original metadata plus + the injected `traceparent`, if one was available or could be created. + :rtype: dict[str, str] + :Example: + + >>> metadata = {"key": "value"} + >>> new_metadata = inject_traceparent(metadata, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01") + >>> print(new_metadata) + {"key": "value", "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"} + """ + + if not traceparent: + traceparent = create_traceparent() + + if not traceparent: return metadata + return { + **metadata, + OTEL_TRACEPARENT_KEY: traceparent, + } + + +class HatchetInstrumentor(BaseInstrumentor): # type: ignore[misc] + def __init__( + self, + tracer_provider: TracerProvider | None = None, + meter_provider: MeterProvider | None = None, + ): + """ + Hatchet OpenTelemetry instrumentor. + + The instrumentor provides an OpenTelemetry integration for Hatchet by setting up + tracing and metrics collection. + + :param tracer_provider: TracerProvider | None: The OpenTelemetry TracerProvider to use. + If not provided, the global tracer provider will be used. + :param meter_provider: MeterProvider | None: The OpenTelemetry MeterProvider to use. + If not provided, a no-op meter provider will be used. + """ + + self.tracer_provider = tracer_provider or get_tracer_provider() + self.meter_provider = meter_provider or NoOpMeterProvider() + + super().__init__() + def instrumentation_dependencies(self) -> Collection[str]: return tuple() @@ -154,7 +227,7 @@ async def _wrap_handle_start_step_run( kwargs: Any, ) -> Exception | None: action = args[0] - traceparent = self.parse_carrier_from_metadata(action.additional_metadata) + traceparent = parse_carrier_from_metadata(action.additional_metadata) with self._tracer.start_as_current_span( "hatchet.start_step_run", diff --git a/hatchet_sdk/utils/aio_utils.py b/hatchet_sdk/utils/aio_utils.py index 3f7ac3f3..459205f1 100644 --- a/hatchet_sdk/utils/aio_utils.py +++ b/hatchet_sdk/utils/aio_utils.py @@ -92,7 +92,7 @@ def __init__(self) -> None: self.loop = asyncio.new_event_loop() self.thread = Thread(target=self.run_loop_in_thread, args=(self.loop,)) - def __enter__(self) -> asyncio.AbstractEventLoop: + def __enter__(self, *a, **kw) -> asyncio.AbstractEventLoop: """ Starts the thread running the event loop when entering the context. @@ -102,7 +102,7 @@ def __enter__(self) -> asyncio.AbstractEventLoop: self.thread.start() return self.loop - def __exit__(self) -> None: + def __exit__(self, *a, **kw) -> None: """ Stops the event loop and joins the thread when exiting the context. """ diff --git a/hatchet_sdk/worker/runner/runner.py b/hatchet_sdk/worker/runner/runner.py index 6e27edd3..01e61bcf 100644 --- a/hatchet_sdk/worker/runner/runner.py +++ b/hatchet_sdk/worker/runner/runner.py @@ -3,12 +3,13 @@ import ctypes import functools import json +import time import traceback from concurrent.futures import ThreadPoolExecutor from enum import Enum from multiprocessing import Queue from threading import Thread, current_thread -from typing import Any, Callable, Dict, Literal, Type, TypeVar, cast, overload +from typing import Any, Callable, Dict, cast from pydantic import BaseModel @@ -421,6 +422,11 @@ async def handle_cancel_action(self, run_id: str) -> None: # check if thread is still running, if so, print a warning if run_id in self.threads: + thread = self.threads.get(run_id) + if thread and self.client.config.enable_force_kill_sync_threads: + self.force_kill_thread(thread) + await asyncio.sleep(1) + logger.warning( f"Thread {self.threads[run_id].ident} with run id {run_id} is still running after cancellation. This could cause the thread pool to get blocked and prevent new tasks from running." ) diff --git a/hatchet_sdk/workflow_run.py b/hatchet_sdk/workflow_run.py index 51a23821..064f6741 100644 --- a/hatchet_sdk/workflow_run.py +++ b/hatchet_sdk/workflow_run.py @@ -32,16 +32,18 @@ def result(self) -> Coroutine: return self.workflow_listener.result(self.workflow_run_id) def sync_result(self) -> dict: + coro = self.workflow_listener.result(self.workflow_run_id) loop = get_active_event_loop() + if loop is None: - with EventLoopThread() as loop: - coro = self.workflow_listener.result(self.workflow_run_id) - future = asyncio.run_coroutine_threadsafe(coro, loop) - return future.result() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(coro) + finally: + asyncio.set_event_loop(None) else: - coro = self.workflow_listener.result(self.workflow_run_id) - future = asyncio.run_coroutine_threadsafe(coro, loop) - return future.result() + return loop.run_until_complete(coro) T = TypeVar("T") diff --git a/poetry.lock b/poetry.lock index f6aa79cd..eb201cc9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -201,21 +201,6 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] -[[package]] -name = "babel" -version = "2.16.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, -] - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - [[package]] name = "black" version = "24.10.0" @@ -265,32 +250,32 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cel-python" -version = "0.1.5" -description = "Pure Python CEL Implementation" +version = "0.2.0" +description = "Pure Python implementation of Google Common Expression Language" optional = false -python-versions = ">=3.7, <4" +python-versions = "<4.0,>=3.8" groups = ["main"] files = [ - {file = "cel-python-0.1.5.tar.gz", hash = "sha256:d3911bb046bc3ed12792bd88ab453f72d98c66923b72a2fa016bcdffd96e2f98"}, - {file = "cel_python-0.1.5-py3-none-any.whl", hash = "sha256:ac81fab8ba08b633700a45d84905be2863529c6a32935c9da7ef53fc06844f1a"}, + {file = "cel_python-0.2.0-py3-none-any.whl", hash = "sha256:478ff73def7b39d51e6982f95d937a57c2b088c491c578fe5cecdbd79f476f60"}, + {file = "cel_python-0.2.0.tar.gz", hash = "sha256:75de72a5cf223ec690b236f0cc24da267219e667bd3e7f8f4f20595fcc1c0c0f"}, ] [package.dependencies] -babel = ">=2.9.0" -jmespath = ">=0.10.0" -lark-parser = ">=0.10.1" -python-dateutil = ">=2.8.1" -pyyaml = ">=5.4.1" -requests = ">=2.25.1" -urllib3 = ">=1.26.4" +jmespath = ">=1.0.1,<2.0.0" +lark = ">=0.12.0,<0.13.0" +python-dateutil = ">=2.9.0.post0,<3.0.0" +pyyaml = ">=6.0.1,<7.0.0" +types-python-dateutil = ">=2.9.0.20240316,<3.0.0.0" +types-pyyaml = ">=6.0.12.20240311,<7.0.0.0" [[package]] name = "certifi" version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." -optional = false +optional = true python-versions = ">=3.6" groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -300,9 +285,10 @@ files = [ name = "charset-normalizer" version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false +optional = true python-versions = ">=3.7" groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -800,15 +786,15 @@ files = [ ] [[package]] -name = "lark-parser" +name = "lark" version = "0.12.0" description = "a modern parsing library" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "lark-parser-0.12.0.tar.gz", hash = "sha256:15967db1f1214013dca65b1180745047b9be457d73da224fcda3d9dd4e96a138"}, - {file = "lark_parser-0.12.0-py2.py3-none-any.whl", hash = "sha256:0eaf30cb5ba787fe404d73a7d6e61df97b21d5a63ac26c5008c78a494373c675"}, + {file = "lark-0.12.0-py2.py3-none-any.whl", hash = "sha256:ed1d891cbcf5151ead1c1d14663bf542443e579e63a76ae175b01b899bd854ca"}, + {file = "lark-0.12.0.tar.gz", hash = "sha256:7da76fcfddadabbbbfd949bbae221efd33938451d90b1fefbbc423c3cccf48ef"}, ] [package.extras] @@ -1695,9 +1681,10 @@ files = [ name = "requests" version = "2.32.3" description = "Python HTTP for Humans." -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"otel\"" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1817,6 +1804,30 @@ files = [ {file = "types_protobuf-5.29.1.20241207.tar.gz", hash = "sha256:2ebcadb8ab3ef2e3e2f067e0882906d64ba0dc65fc5b0fd7a8b692315b4a0be9"}, ] +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20241206" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20241230" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6"}, + {file = "types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -2047,4 +2058,4 @@ otel = ["opentelemetry-api", "opentelemetry-distro", "opentelemetry-exporter-otl [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "0d25006ef0b235347f4183edbf435d4accccda2b9f554eb8f77067d4178e731c" +content-hash = "8c698a69cdddae4857f547cb9f178d56779d2f5a6431bd72e13e0a7a7b36176f" diff --git a/pyproject.toml b/pyproject.toml index 29cc6578..8123f1a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "hatchet-sdk" -version = "0.46.1" +version = "0.47.0" description = "" authors = ["Alexander Belanger "] readme = "README.md" @@ -27,7 +27,7 @@ nest-asyncio = "^1.6.0" aiohttp = "^3.10.5" aiohttp-retry = "^2.8.3" tenacity = ">=8.4.1" -cel-python = "^0.1.5" +cel-python = "^0.2.0" opentelemetry-api = { version = "^1.28.0", optional = true } opentelemetry-sdk = { version = "^1.28.0", optional = true } opentelemetry-instrumentation = { version = ">=0.49b0", optional = true } @@ -111,6 +111,7 @@ explicit_package_bases = true api = "examples.api.api:main" async = "examples.async.worker:main" fanout = "examples.fanout.worker:main" +fanout_sync = "examples.fanout_sync.worker:main" cancellation = "examples.cancellation.worker:main" concurrency_limit = "examples.concurrency_limit.worker:main" concurrency_limit_rr = "examples.concurrency_limit_rr.worker:main"