nora-lib 0.0.1.dev0__tar.gz → 0.0.2__tar.gz

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.
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nora_lib
3
- Version: 0.0.1.dev0
3
+ Version: 0.0.2
4
4
  Summary: For making and coordinating agents and tools
5
5
  Home-page: https://github.com/allenai/nora_lib
6
6
  Requires-Python: >=3.9
7
+ Requires-Dist: pydantic<3,>=2
7
8
  Provides-Extra: dev
8
9
  Requires-Dist: mypy; extra == "dev"
9
10
  Requires-Dist: pytest; extra == "dev"
File without changes
@@ -0,0 +1,25 @@
1
+ from typing import Any, Dict, Generic, Optional, TypeVar
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ R = TypeVar("R", bound=BaseModel)
6
+
7
+
8
+ TASK_STATUSES = dict(STARTED="STARTED", FAILED="FAILED", COMPLETED="COMPLETED")
9
+
10
+
11
+ class AsyncTaskState(BaseModel, Generic[R]):
12
+ """Models the current state of an asynchronous request."""
13
+
14
+ task_id: str = Field(
15
+ "Identifies the long-running task so that its status and eventual result"
16
+ "can be checked in follow-up calls."
17
+ )
18
+ estimated_time: str = Field(
19
+ description="How long we expect this task to take from start to finish."
20
+ )
21
+ task_status: str = Field(description="Current human-readable status of the task.")
22
+ task_result: Optional[R] = Field(description="Final result of the task.")
23
+ extra_state: Dict[str, Any] = Field(
24
+ description="Any extra task-specific state can go in here as free-form JSON-serializable dictionary."
25
+ )
@@ -0,0 +1,53 @@
1
+ """
2
+ Handles IO for asynchronous task-related state.
3
+ Currently just reads/writes from local disk,
4
+ the best and most robust mechanism.
5
+
6
+ A StateManager instance must be initialized with
7
+ a concrete subclass of `AsyncTaskState`, as implemented
8
+ by dependent projects.
9
+ """
10
+
11
+ import json
12
+ import os
13
+ from typing import Generic, Optional, Type
14
+
15
+ from nora_lib.tasks.models import AsyncTaskState, R, TASK_STATUSES
16
+
17
+
18
+ class NoSuchTaskException(Exception):
19
+ def __init__(self, task_id: str):
20
+ self._task_id = task_id
21
+
22
+ def __str__(self):
23
+ return f"No record found for task {self._task_id}"
24
+
25
+
26
+ class StateManager(Generic[R]):
27
+ def __init__(self, task_state_class: Type[AsyncTaskState[R]], state_dir) -> None:
28
+ self._task_state_class = task_state_class
29
+ self._state_dir = state_dir
30
+
31
+ def read_state(self, task_id: str) -> AsyncTaskState[R]:
32
+ task_state_path = os.path.join(self._state_dir, f"{task_id}.json")
33
+ if not os.path.isfile(task_state_path):
34
+ raise NoSuchTaskException(task_id)
35
+
36
+ with open(task_state_path, "r") as f:
37
+ return self._task_state_class(**json.loads(f.read()))
38
+
39
+ def write_state(self, state: AsyncTaskState[R]) -> None:
40
+ task_state_path = os.path.join(self._state_dir, f"{state.task_id}.json")
41
+ with open(task_state_path, "w") as f:
42
+ json.dump(state.model_dump(), f)
43
+
44
+ def update_status(self, task_id: str, new_status: str) -> None:
45
+ state = self.read_state(task_id)
46
+ state.task_status = new_status
47
+ self.write_state(state)
48
+
49
+ def save_result(self, task_id: str, task_result: R) -> None:
50
+ state = self.read_state(task_id)
51
+ state.task_status = TASK_STATUSES["COMPLETED"]
52
+ state.task_result = task_result
53
+ self.write_state(state)
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nora_lib
3
- Version: 0.0.1.dev0
3
+ Version: 0.0.2
4
4
  Summary: For making and coordinating agents and tools
5
5
  Home-page: https://github.com/allenai/nora_lib
6
6
  Requires-Python: >=3.9
7
+ Requires-Dist: pydantic<3,>=2
7
8
  Provides-Extra: dev
8
9
  Requires-Dist: mypy; extra == "dev"
9
10
  Requires-Dist: pytest; extra == "dev"
@@ -7,4 +7,10 @@ nora_lib.egg-info/PKG-INFO
7
7
  nora_lib.egg-info/SOURCES.txt
8
8
  nora_lib.egg-info/dependency_links.txt
9
9
  nora_lib.egg-info/requires.txt
10
- nora_lib.egg-info/top_level.txt
10
+ nora_lib.egg-info/top_level.txt
11
+ nora_lib/tasks/__init__.py
12
+ nora_lib/tasks/models.py
13
+ nora_lib/tasks/state.py
14
+ tests/test_placeholder.py
15
+ tests/tasks/__init__.py
16
+ tests/tasks/test_state.py
@@ -1,3 +1,4 @@
1
+ pydantic<3,>=2
1
2
 
2
3
  [dev]
3
4
  mypy
@@ -1,13 +1,13 @@
1
1
  import setuptools
2
2
 
3
- runtime_requirements = []
3
+ runtime_requirements = ["pydantic>=2,<3"]
4
4
 
5
5
  # For running tests, linting, etc
6
6
  dev_requirements = ["mypy", "pytest", "black"]
7
7
 
8
8
  setuptools.setup(
9
9
  name="nora_lib",
10
- version="0.0.1.dev0",
10
+ version="0.0.2",
11
11
  description="For making and coordinating agents and tools",
12
12
  url="https://github.com/allenai/nora_lib",
13
13
  packages=setuptools.find_packages(exclude=(["tests"])),
File without changes
@@ -0,0 +1,77 @@
1
+ import tempfile
2
+ import unittest
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from nora_lib.tasks.models import AsyncTaskState, TASK_STATUSES
7
+ from nora_lib.tasks.state import NoSuchTaskException, StateManager
8
+
9
+
10
+ class MyTaskResult(BaseModel):
11
+ a: str
12
+ b: int
13
+
14
+
15
+ class MyAsyncTaskState(AsyncTaskState[MyTaskResult]):
16
+ pass
17
+
18
+
19
+ class TestState(unittest.TestCase):
20
+ def test__can_read_and_write_state(self):
21
+ with tempfile.TemporaryDirectory() as tmpdir:
22
+ manager = StateManager(MyAsyncTaskState, tmpdir)
23
+ state = MyAsyncTaskState(
24
+ task_id="asdf",
25
+ estimated_time="40 days and 40 nights",
26
+ task_status="STARTED",
27
+ task_result=None,
28
+ extra_state={"foo": "bar"},
29
+ )
30
+ manager.write_state(state)
31
+ fetched_state = manager.read_state("asdf")
32
+
33
+ self.assertEqual(state, fetched_state)
34
+
35
+ def test__raises_an_error_if_referencing_nonexistent_task(self):
36
+ with tempfile.TemporaryDirectory() as tmpdir:
37
+ manager = StateManager(MyAsyncTaskState, tmpdir)
38
+ with self.assertRaises(NoSuchTaskException):
39
+ manager.read_state("asdf")
40
+
41
+ def test__allows_specific_update_of_status_field(self):
42
+ with tempfile.TemporaryDirectory() as tmpdir:
43
+ manager = StateManager(MyAsyncTaskState, tmpdir)
44
+ state = MyAsyncTaskState(
45
+ task_id="asdf",
46
+ estimated_time="40 days and 40 nights",
47
+ task_status="STARTED",
48
+ task_result=None,
49
+ extra_state={"foo": "bar"},
50
+ )
51
+ manager.write_state(state)
52
+ manager.update_status(state.task_id, "fail")
53
+ fetched_state = manager.read_state("asdf")
54
+ state.task_status = "fail"
55
+
56
+ self.assertEqual(state, fetched_state)
57
+
58
+ def test__allows_specific_update_of_result_field(self):
59
+ with tempfile.TemporaryDirectory() as tmpdir:
60
+ manager = StateManager(MyAsyncTaskState, tmpdir)
61
+ state = MyAsyncTaskState(
62
+ task_id="asdf",
63
+ estimated_time="40 days and 40 nights",
64
+ task_status="STARTED",
65
+ task_result=None,
66
+ extra_state={"foo": "bar"},
67
+ )
68
+ manager.write_state(state)
69
+
70
+ result = MyTaskResult(a="asdf", b=123)
71
+ manager.save_result(state.task_id, result)
72
+ fetched_state = manager.read_state("asdf")
73
+
74
+ state.task_status = TASK_STATUSES["COMPLETED"]
75
+ state.task_result = result
76
+
77
+ self.assertEqual(state, fetched_state)
@@ -0,0 +1,6 @@
1
+ import unittest
2
+
3
+
4
+ class TestPlaceholder(unittest.TestCase):
5
+ def test_placeholder(self):
6
+ self.assertEqual(1, 1)
File without changes
File without changes
File without changes