hatchet-sdk 1.16.4__py3-none-any.whl → 1.17.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.

Potentially problematic release.


This version of hatchet-sdk might be problematic. Click here for more details.

Files changed (38) hide show
  1. hatchet_sdk/__init__.py +2 -1
  2. hatchet_sdk/clients/events.py +5 -2
  3. hatchet_sdk/clients/rest/__init__.py +32 -0
  4. hatchet_sdk/clients/rest/api/__init__.py +1 -0
  5. hatchet_sdk/clients/rest/api/webhook_api.py +1551 -0
  6. hatchet_sdk/clients/rest/models/__init__.py +31 -0
  7. hatchet_sdk/clients/rest/models/v1_create_webhook_request.py +215 -0
  8. hatchet_sdk/clients/rest/models/v1_create_webhook_request_api_key.py +126 -0
  9. hatchet_sdk/clients/rest/models/v1_create_webhook_request_api_key_all_of_auth_type.py +82 -0
  10. hatchet_sdk/clients/rest/models/v1_create_webhook_request_base.py +98 -0
  11. hatchet_sdk/clients/rest/models/v1_create_webhook_request_basic_auth.py +126 -0
  12. hatchet_sdk/clients/rest/models/v1_create_webhook_request_basic_auth_all_of_auth_type.py +82 -0
  13. hatchet_sdk/clients/rest/models/v1_create_webhook_request_hmac.py +126 -0
  14. hatchet_sdk/clients/rest/models/v1_create_webhook_request_hmac_all_of_auth_type.py +82 -0
  15. hatchet_sdk/clients/rest/models/v1_event.py +7 -0
  16. hatchet_sdk/clients/rest/models/v1_webhook.py +126 -0
  17. hatchet_sdk/clients/rest/models/v1_webhook_api_key_auth.py +90 -0
  18. hatchet_sdk/clients/rest/models/v1_webhook_auth_type.py +38 -0
  19. hatchet_sdk/clients/rest/models/v1_webhook_basic_auth.py +86 -0
  20. hatchet_sdk/clients/rest/models/v1_webhook_hmac_algorithm.py +39 -0
  21. hatchet_sdk/clients/rest/models/v1_webhook_hmac_auth.py +115 -0
  22. hatchet_sdk/clients/rest/models/v1_webhook_hmac_encoding.py +38 -0
  23. hatchet_sdk/clients/rest/models/v1_webhook_list.py +110 -0
  24. hatchet_sdk/clients/rest/models/v1_webhook_receive200_response.py +83 -0
  25. hatchet_sdk/clients/rest/models/v1_webhook_source_name.py +38 -0
  26. hatchet_sdk/context/context.py +31 -2
  27. hatchet_sdk/exceptions.py +70 -5
  28. hatchet_sdk/hatchet.py +29 -11
  29. hatchet_sdk/rate_limit.py +1 -21
  30. hatchet_sdk/runnables/task.py +109 -19
  31. hatchet_sdk/runnables/workflow.py +23 -8
  32. hatchet_sdk/utils/typing.py +27 -0
  33. hatchet_sdk/worker/runner/runner.py +27 -19
  34. hatchet_sdk/worker/runner/utils/capture_logs.py +24 -11
  35. {hatchet_sdk-1.16.4.dist-info → hatchet_sdk-1.17.0.dist-info}/METADATA +2 -3
  36. {hatchet_sdk-1.16.4.dist-info → hatchet_sdk-1.17.0.dist-info}/RECORD +38 -19
  37. {hatchet_sdk-1.16.4.dist-info → hatchet_sdk-1.17.0.dist-info}/WHEEL +0 -0
  38. {hatchet_sdk-1.16.4.dist-info → hatchet_sdk-1.17.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,115 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Hatchet API
5
+
6
+ The Hatchet API
7
+
8
+ The version of the OpenAPI document: 1.0.0
9
+ Generated by OpenAPI Generator (https://openapi-generator.tech)
10
+
11
+ Do not edit the class manually.
12
+ """ # noqa: E501
13
+
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import pprint
19
+ import re # noqa: F401
20
+ from typing import Any, ClassVar, Dict, List, Optional, Set
21
+
22
+ from pydantic import BaseModel, ConfigDict, Field, StrictStr
23
+ from typing_extensions import Self
24
+
25
+ from hatchet_sdk.clients.rest.models.v1_webhook_hmac_algorithm import (
26
+ V1WebhookHMACAlgorithm,
27
+ )
28
+ from hatchet_sdk.clients.rest.models.v1_webhook_hmac_encoding import (
29
+ V1WebhookHMACEncoding,
30
+ )
31
+
32
+
33
+ class V1WebhookHMACAuth(BaseModel):
34
+ """
35
+ V1WebhookHMACAuth
36
+ """ # noqa: E501
37
+
38
+ algorithm: V1WebhookHMACAlgorithm = Field(
39
+ description="The HMAC algorithm to use for the webhook"
40
+ )
41
+ encoding: V1WebhookHMACEncoding = Field(
42
+ description="The encoding to use for the HMAC signature"
43
+ )
44
+ signature_header_name: StrictStr = Field(
45
+ description="The name of the header to use for the HMAC signature",
46
+ alias="signatureHeaderName",
47
+ )
48
+ signing_secret: StrictStr = Field(
49
+ description="The secret key used to sign the HMAC signature",
50
+ alias="signingSecret",
51
+ )
52
+ __properties: ClassVar[List[str]] = [
53
+ "algorithm",
54
+ "encoding",
55
+ "signatureHeaderName",
56
+ "signingSecret",
57
+ ]
58
+
59
+ model_config = ConfigDict(
60
+ populate_by_name=True,
61
+ validate_assignment=True,
62
+ protected_namespaces=(),
63
+ )
64
+
65
+ def to_str(self) -> str:
66
+ """Returns the string representation of the model using alias"""
67
+ return pprint.pformat(self.model_dump(by_alias=True))
68
+
69
+ def to_json(self) -> str:
70
+ """Returns the JSON representation of the model using alias"""
71
+ # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
72
+ return json.dumps(self.to_dict())
73
+
74
+ @classmethod
75
+ def from_json(cls, json_str: str) -> Optional[Self]:
76
+ """Create an instance of V1WebhookHMACAuth from a JSON string"""
77
+ return cls.from_dict(json.loads(json_str))
78
+
79
+ def to_dict(self) -> Dict[str, Any]:
80
+ """Return the dictionary representation of the model using alias.
81
+
82
+ This has the following differences from calling pydantic's
83
+ `self.model_dump(by_alias=True)`:
84
+
85
+ * `None` is only added to the output dict for nullable fields that
86
+ were set at model initialization. Other fields with value `None`
87
+ are ignored.
88
+ """
89
+ excluded_fields: Set[str] = set([])
90
+
91
+ _dict = self.model_dump(
92
+ by_alias=True,
93
+ exclude=excluded_fields,
94
+ exclude_none=True,
95
+ )
96
+ return _dict
97
+
98
+ @classmethod
99
+ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
100
+ """Create an instance of V1WebhookHMACAuth from a dict"""
101
+ if obj is None:
102
+ return None
103
+
104
+ if not isinstance(obj, dict):
105
+ return cls.model_validate(obj)
106
+
107
+ _obj = cls.model_validate(
108
+ {
109
+ "algorithm": obj.get("algorithm"),
110
+ "encoding": obj.get("encoding"),
111
+ "signatureHeaderName": obj.get("signatureHeaderName"),
112
+ "signingSecret": obj.get("signingSecret"),
113
+ }
114
+ )
115
+ return _obj
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Hatchet API
5
+
6
+ The Hatchet API
7
+
8
+ The version of the OpenAPI document: 1.0.0
9
+ Generated by OpenAPI Generator (https://openapi-generator.tech)
10
+
11
+ Do not edit the class manually.
12
+ """ # noqa: E501
13
+
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from enum import Enum
19
+
20
+ from typing_extensions import Self
21
+
22
+
23
+ class V1WebhookHMACEncoding(str, Enum):
24
+ """
25
+ V1WebhookHMACEncoding
26
+ """
27
+
28
+ """
29
+ allowed enum values
30
+ """
31
+ HEX = "HEX"
32
+ BASE64 = "BASE64"
33
+ BASE64URL = "BASE64URL"
34
+
35
+ @classmethod
36
+ def from_json(cls, json_str: str) -> Self:
37
+ """Create an instance of V1WebhookHMACEncoding from a JSON string"""
38
+ return cls(json.loads(json_str))
@@ -0,0 +1,110 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Hatchet API
5
+
6
+ The Hatchet API
7
+
8
+ The version of the OpenAPI document: 1.0.0
9
+ Generated by OpenAPI Generator (https://openapi-generator.tech)
10
+
11
+ Do not edit the class manually.
12
+ """ # noqa: E501
13
+
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import pprint
19
+ import re # noqa: F401
20
+ from typing import Any, ClassVar, Dict, List, Optional, Set
21
+
22
+ from pydantic import BaseModel, ConfigDict
23
+ from typing_extensions import Self
24
+
25
+ from hatchet_sdk.clients.rest.models.pagination_response import PaginationResponse
26
+ from hatchet_sdk.clients.rest.models.v1_webhook import V1Webhook
27
+
28
+
29
+ class V1WebhookList(BaseModel):
30
+ """
31
+ V1WebhookList
32
+ """ # noqa: E501
33
+
34
+ pagination: Optional[PaginationResponse] = None
35
+ rows: Optional[List[V1Webhook]] = None
36
+ __properties: ClassVar[List[str]] = ["pagination", "rows"]
37
+
38
+ model_config = ConfigDict(
39
+ populate_by_name=True,
40
+ validate_assignment=True,
41
+ protected_namespaces=(),
42
+ )
43
+
44
+ def to_str(self) -> str:
45
+ """Returns the string representation of the model using alias"""
46
+ return pprint.pformat(self.model_dump(by_alias=True))
47
+
48
+ def to_json(self) -> str:
49
+ """Returns the JSON representation of the model using alias"""
50
+ # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
51
+ return json.dumps(self.to_dict())
52
+
53
+ @classmethod
54
+ def from_json(cls, json_str: str) -> Optional[Self]:
55
+ """Create an instance of V1WebhookList from a JSON string"""
56
+ return cls.from_dict(json.loads(json_str))
57
+
58
+ def to_dict(self) -> Dict[str, Any]:
59
+ """Return the dictionary representation of the model using alias.
60
+
61
+ This has the following differences from calling pydantic's
62
+ `self.model_dump(by_alias=True)`:
63
+
64
+ * `None` is only added to the output dict for nullable fields that
65
+ were set at model initialization. Other fields with value `None`
66
+ are ignored.
67
+ """
68
+ excluded_fields: Set[str] = set([])
69
+
70
+ _dict = self.model_dump(
71
+ by_alias=True,
72
+ exclude=excluded_fields,
73
+ exclude_none=True,
74
+ )
75
+ # override the default output from pydantic by calling `to_dict()` of pagination
76
+ if self.pagination:
77
+ _dict["pagination"] = self.pagination.to_dict()
78
+ # override the default output from pydantic by calling `to_dict()` of each item in rows (list)
79
+ _items = []
80
+ if self.rows:
81
+ for _item_rows in self.rows:
82
+ if _item_rows:
83
+ _items.append(_item_rows.to_dict())
84
+ _dict["rows"] = _items
85
+ return _dict
86
+
87
+ @classmethod
88
+ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
89
+ """Create an instance of V1WebhookList from a dict"""
90
+ if obj is None:
91
+ return None
92
+
93
+ if not isinstance(obj, dict):
94
+ return cls.model_validate(obj)
95
+
96
+ _obj = cls.model_validate(
97
+ {
98
+ "pagination": (
99
+ PaginationResponse.from_dict(obj["pagination"])
100
+ if obj.get("pagination") is not None
101
+ else None
102
+ ),
103
+ "rows": (
104
+ [V1Webhook.from_dict(_item) for _item in obj["rows"]]
105
+ if obj.get("rows") is not None
106
+ else None
107
+ ),
108
+ }
109
+ )
110
+ return _obj
@@ -0,0 +1,83 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Hatchet API
5
+
6
+ The Hatchet API
7
+
8
+ The version of the OpenAPI document: 1.0.0
9
+ Generated by OpenAPI Generator (https://openapi-generator.tech)
10
+
11
+ Do not edit the class manually.
12
+ """ # noqa: E501
13
+
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import pprint
19
+ import re # noqa: F401
20
+ from typing import Any, ClassVar, Dict, List, Optional, Set
21
+
22
+ from pydantic import BaseModel, ConfigDict, StrictStr
23
+ from typing_extensions import Self
24
+
25
+
26
+ class V1WebhookReceive200Response(BaseModel):
27
+ """
28
+ V1WebhookReceive200Response
29
+ """ # noqa: E501
30
+
31
+ message: Optional[StrictStr] = None
32
+ __properties: ClassVar[List[str]] = ["message"]
33
+
34
+ model_config = ConfigDict(
35
+ populate_by_name=True,
36
+ validate_assignment=True,
37
+ protected_namespaces=(),
38
+ )
39
+
40
+ def to_str(self) -> str:
41
+ """Returns the string representation of the model using alias"""
42
+ return pprint.pformat(self.model_dump(by_alias=True))
43
+
44
+ def to_json(self) -> str:
45
+ """Returns the JSON representation of the model using alias"""
46
+ # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
47
+ return json.dumps(self.to_dict())
48
+
49
+ @classmethod
50
+ def from_json(cls, json_str: str) -> Optional[Self]:
51
+ """Create an instance of V1WebhookReceive200Response from a JSON string"""
52
+ return cls.from_dict(json.loads(json_str))
53
+
54
+ def to_dict(self) -> Dict[str, Any]:
55
+ """Return the dictionary representation of the model using alias.
56
+
57
+ This has the following differences from calling pydantic's
58
+ `self.model_dump(by_alias=True)`:
59
+
60
+ * `None` is only added to the output dict for nullable fields that
61
+ were set at model initialization. Other fields with value `None`
62
+ are ignored.
63
+ """
64
+ excluded_fields: Set[str] = set([])
65
+
66
+ _dict = self.model_dump(
67
+ by_alias=True,
68
+ exclude=excluded_fields,
69
+ exclude_none=True,
70
+ )
71
+ return _dict
72
+
73
+ @classmethod
74
+ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
75
+ """Create an instance of V1WebhookReceive200Response from a dict"""
76
+ if obj is None:
77
+ return None
78
+
79
+ if not isinstance(obj, dict):
80
+ return cls.model_validate(obj)
81
+
82
+ _obj = cls.model_validate({"message": obj.get("message")})
83
+ return _obj
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Hatchet API
5
+
6
+ The Hatchet API
7
+
8
+ The version of the OpenAPI document: 1.0.0
9
+ Generated by OpenAPI Generator (https://openapi-generator.tech)
10
+
11
+ Do not edit the class manually.
12
+ """ # noqa: E501
13
+
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from enum import Enum
19
+
20
+ from typing_extensions import Self
21
+
22
+
23
+ class V1WebhookSourceName(str, Enum):
24
+ """
25
+ V1WebhookSourceName
26
+ """
27
+
28
+ """
29
+ allowed enum values
30
+ """
31
+ GENERIC = "GENERIC"
32
+ GITHUB = "GITHUB"
33
+ STRIPE = "STRIPE"
34
+
35
+ @classmethod
36
+ def from_json(cls, json_str: str) -> Self:
37
+ """Create an instance of V1WebhookSourceName from a JSON string"""
38
+ return cls(json.loads(json_str))
@@ -21,10 +21,11 @@ from hatchet_sdk.conditions import (
21
21
  flatten_conditions,
22
22
  )
23
23
  from hatchet_sdk.context.worker_context import WorkerContext
24
+ from hatchet_sdk.exceptions import TaskRunError
24
25
  from hatchet_sdk.features.runs import RunsClient
25
26
  from hatchet_sdk.logger import logger
26
27
  from hatchet_sdk.utils.timedelta_to_expression import Duration, timedelta_to_expr
27
- from hatchet_sdk.utils.typing import JSONSerializableMapping
28
+ from hatchet_sdk.utils.typing import JSONSerializableMapping, LogLevel
28
29
  from hatchet_sdk.worker.runner.utils.capture_logs import AsyncLogSender, LogRecord
29
30
 
30
31
  if TYPE_CHECKING:
@@ -211,7 +212,9 @@ class Context:
211
212
  line = str(line)
212
213
 
213
214
  logger.info(line)
214
- self.log_sender.publish(LogRecord(message=line, step_run_id=self.step_run_id))
215
+ self.log_sender.publish(
216
+ LogRecord(message=line, step_run_id=self.step_run_id, level=LogLevel.INFO)
217
+ )
215
218
 
216
219
  def release_slot(self) -> None:
217
220
  """
@@ -360,15 +363,41 @@ class Context:
360
363
  task: "Task[TWorkflowInput, R]",
361
364
  ) -> str | None:
362
365
  """
366
+ **DEPRECATED**: Use `get_task_run_error` instead.
367
+
363
368
  A helper intended to be used in an on-failure step to retrieve the error that occurred in a specific upstream task run.
364
369
 
365
370
  :param task: The task whose error you want to retrieve.
366
371
  :return: The error message of the task run, or None if no error occurred.
367
372
  """
373
+ warn(
374
+ "`fetch_task_run_error` is deprecated. Use `get_task_run_error` instead.",
375
+ DeprecationWarning,
376
+ stacklevel=2,
377
+ )
368
378
  errors = self.data.step_run_errors
369
379
 
370
380
  return errors.get(task.name)
371
381
 
382
+ def get_task_run_error(
383
+ self,
384
+ task: "Task[TWorkflowInput, R]",
385
+ ) -> TaskRunError | None:
386
+ """
387
+ A helper intended to be used in an on-failure step to retrieve the error that occurred in a specific upstream task run.
388
+
389
+ :param task: The task whose error you want to retrieve.
390
+ :return: The error message of the task run, or None if no error occurred.
391
+ """
392
+ errors = self.data.step_run_errors
393
+
394
+ error = errors.get(task.name)
395
+
396
+ if not error:
397
+ return None
398
+
399
+ return TaskRunError.deserialize(error)
400
+
372
401
 
373
402
  class DurableContext(Context):
374
403
  def __init__(
hatchet_sdk/exceptions.py CHANGED
@@ -1,4 +1,10 @@
1
+ import json
1
2
  import traceback
3
+ from typing import cast
4
+
5
+
6
+ class InvalidDependencyError(Exception):
7
+ pass
2
8
 
3
9
 
4
10
  class NonRetryableException(Exception): # noqa: N818
@@ -9,28 +15,42 @@ class DedupeViolationError(Exception):
9
15
  """Raised by the Hatchet library to indicate that a workflow has already been run with this deduplication value."""
10
16
 
11
17
 
18
+ TASK_RUN_ERROR_METADATA_KEY = "__hatchet_error_metadata__"
19
+
20
+
12
21
  class TaskRunError(Exception):
13
22
  def __init__(
14
23
  self,
15
24
  exc: str,
16
25
  exc_type: str,
17
26
  trace: str,
27
+ task_run_external_id: str | None,
18
28
  ) -> None:
19
29
  self.exc = exc
20
30
  self.exc_type = exc_type
21
31
  self.trace = trace
32
+ self.task_run_external_id = task_run_external_id
22
33
 
23
34
  def __str__(self) -> str:
24
- return self.serialize()
35
+ return self.serialize(include_metadata=False)
25
36
 
26
37
  def __repr__(self) -> str:
27
38
  return str(self)
28
39
 
29
- def serialize(self) -> str:
40
+ def serialize(self, include_metadata: bool) -> str:
30
41
  if not self.exc_type or not self.exc:
31
42
  return ""
32
43
 
33
- return (
44
+ metadata = json.dumps(
45
+ {
46
+ TASK_RUN_ERROR_METADATA_KEY: {
47
+ "task_run_external_id": self.task_run_external_id,
48
+ }
49
+ },
50
+ indent=None,
51
+ )
52
+
53
+ result = (
34
54
  self.exc_type.replace(": ", ":::")
35
55
  + ": "
36
56
  + self.exc.replace("\n", "\\\n")
@@ -38,6 +58,40 @@ class TaskRunError(Exception):
38
58
  + self.trace
39
59
  )
40
60
 
61
+ if include_metadata:
62
+ return result + "\n\n" + metadata
63
+
64
+ return result
65
+
66
+ @classmethod
67
+ def _extract_metadata(cls, serialized: str) -> tuple[str, dict[str, str | None]]:
68
+ metadata = serialized.split("\n")[-1]
69
+
70
+ try:
71
+ parsed = json.loads(metadata)
72
+
73
+ if (
74
+ TASK_RUN_ERROR_METADATA_KEY in parsed
75
+ and "task_run_external_id" in parsed[TASK_RUN_ERROR_METADATA_KEY]
76
+ ):
77
+ serialized = serialized.replace(metadata, "").strip()
78
+ return serialized, cast(
79
+ dict[str, str | None], parsed[TASK_RUN_ERROR_METADATA_KEY]
80
+ )
81
+
82
+ return serialized, {}
83
+ except json.JSONDecodeError:
84
+ return serialized, {}
85
+
86
+ @classmethod
87
+ def _unpack_serialized_error(cls, serialized: str) -> tuple[str | None, str, str]:
88
+ serialized, metadata = cls._extract_metadata(serialized)
89
+
90
+ external_id = metadata.get("task_run_external_id", None)
91
+ header, trace = serialized.split("\n", 1)
92
+
93
+ return external_id, header, trace
94
+
41
95
  @classmethod
42
96
  def deserialize(cls, serialized: str) -> "TaskRunError":
43
97
  if not serialized:
@@ -45,10 +99,16 @@ class TaskRunError(Exception):
45
99
  exc="",
46
100
  exc_type="",
47
101
  trace="",
102
+ task_run_external_id=None,
48
103
  )
49
104
 
105
+ task_run_external_id = None
106
+
50
107
  try:
51
- header, trace = serialized.split("\n", 1)
108
+ task_run_external_id, header, trace = cls._unpack_serialized_error(
109
+ serialized
110
+ )
111
+
52
112
  exc_type, exc = header.split(": ", 1)
53
113
  except ValueError:
54
114
  ## If we get here, we saw an error that was not serialized how we expected,
@@ -57,6 +117,7 @@ class TaskRunError(Exception):
57
117
  exc=serialized,
58
118
  exc_type="HatchetError",
59
119
  trace="",
120
+ task_run_external_id=task_run_external_id,
60
121
  )
61
122
 
62
123
  exc_type = exc_type.replace(":::", ": ")
@@ -66,16 +127,20 @@ class TaskRunError(Exception):
66
127
  exc=exc,
67
128
  exc_type=exc_type,
68
129
  trace=trace,
130
+ task_run_external_id=task_run_external_id,
69
131
  )
70
132
 
71
133
  @classmethod
72
- def from_exception(cls, exc: Exception) -> "TaskRunError":
134
+ def from_exception(
135
+ cls, exc: Exception, task_run_external_id: str | None
136
+ ) -> "TaskRunError":
73
137
  return cls(
74
138
  exc=str(exc),
75
139
  exc_type=type(exc).__name__,
76
140
  trace="".join(
77
141
  traceback.format_exception(type(exc), exc, exc.__traceback__)
78
142
  ),
143
+ task_run_external_id=task_run_external_id,
79
144
  )
80
145
 
81
146