value-stream 0.1.1__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.
Files changed (81) hide show
  1. value_stream/__init__.py +1 -0
  2. value_stream/app/SPEC.md +79 -0
  3. value_stream/app/__init__.py +1 -0
  4. value_stream/app/__main__.py +28 -0
  5. value_stream/app/contracts.py +18 -0
  6. value_stream/app/coordinator.py +359 -0
  7. value_stream/app/errors.py +17 -0
  8. value_stream/app/exports.py +142 -0
  9. value_stream/app/gateway.py +130 -0
  10. value_stream/app/generation.py +269 -0
  11. value_stream/app/insights.py +75 -0
  12. value_stream/app/metrics.py +132 -0
  13. value_stream/app/openapi.json +3699 -0
  14. value_stream/app/schemas.py +288 -0
  15. value_stream/app/server.py +298 -0
  16. value_stream/app/settings.py +46 -0
  17. value_stream/app/storage.py +651 -0
  18. value_stream/client/__init__.py +3 -0
  19. value_stream/client/simulation_runner.py +83 -0
  20. value_stream/client/views/__init__.py +4 -0
  21. value_stream/client/views/metadata_viewer.py +235 -0
  22. value_stream/client/views/result_viewer.py +76 -0
  23. value_stream/client/views/viewer.py +8 -0
  24. value_stream/client/web/__init__.py +10 -0
  25. value_stream/client/web/client.py +115 -0
  26. value_stream/client/web/errors.py +29 -0
  27. value_stream/client/web/local_service.py +92 -0
  28. value_stream/factory/__init__.py +5 -0
  29. value_stream/factory/developer_factory.py +33 -0
  30. value_stream/factory/factory.py +43 -0
  31. value_stream/factory/task_factory.py +64 -0
  32. value_stream/factory/task_generator.py +102 -0
  33. value_stream/policy/__init__.py +3 -0
  34. value_stream/policy/simulation_policy.py +20 -0
  35. value_stream/resources/__init__.py +23 -0
  36. value_stream/resources/developer.py +53 -0
  37. value_stream/resources/qa_tester.py +51 -0
  38. value_stream/resources/resource.py +162 -0
  39. value_stream/resources/resource_metadata.py +24 -0
  40. value_stream/resources/resource_policy.py +13 -0
  41. value_stream/resources/resource_pool.py +39 -0
  42. value_stream/resources/resource_tracker.py +160 -0
  43. value_stream/resources/toolchain.py +43 -0
  44. value_stream/service/SPEC.md +46 -0
  45. value_stream/service/__init__.py +1 -0
  46. value_stream/service/app.py +218 -0
  47. value_stream/service/codec.py +237 -0
  48. value_stream/service/job_store.py +322 -0
  49. value_stream/service/openapi.json +1283 -0
  50. value_stream/service/scheduler.py +234 -0
  51. value_stream/service/schemas.py +248 -0
  52. value_stream/service/settings.py +37 -0
  53. value_stream/service/worker.py +82 -0
  54. value_stream/simulation/__init__.py +16 -0
  55. value_stream/simulation/default_simulation_policy.py +68 -0
  56. value_stream/simulation/model.py +30 -0
  57. value_stream/simulation/model_factory.py +54 -0
  58. value_stream/simulation/simulation.py +144 -0
  59. value_stream/simulation/simulation_metadata.py +12 -0
  60. value_stream/simulation/simulation_result.py +20 -0
  61. value_stream/task/__init__.py +23 -0
  62. value_stream/task/epoch.py +21 -0
  63. value_stream/task/event_status.py +8 -0
  64. value_stream/task/task.py +260 -0
  65. value_stream/task/task_event.py +97 -0
  66. value_stream/task/task_history.py +208 -0
  67. value_stream/task/task_router.py +70 -0
  68. value_stream/task/task_state.py +7 -0
  69. value_stream/task/task_type.py +8 -0
  70. value_stream/workflow/__init__.py +18 -0
  71. value_stream/workflow/assignment_strategy.py +10 -0
  72. value_stream/workflow/pool_manager.py +136 -0
  73. value_stream/workflow/resource_operator.py +204 -0
  74. value_stream/workflow/sdlc_workflow.py +99 -0
  75. value_stream/workflow/support_workflow.py +23 -0
  76. value_stream/workflow/task_store.py +125 -0
  77. value_stream/workflow/workflow_policy.py +12 -0
  78. value_stream-0.1.1.dist-info/METADATA +288 -0
  79. value_stream-0.1.1.dist-info/RECORD +81 -0
  80. value_stream-0.1.1.dist-info/WHEEL +4 -0
  81. value_stream-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,33 @@
1
+ from typing import Any
2
+ from value_stream.resources import Developer
3
+
4
+ from .factory import Factory
5
+
6
+
7
+ class DeveloperFactory(Factory):
8
+ """Utility for creating Developer objects"""
9
+
10
+ @classmethod
11
+ def create(cls, count: int, **kwargs: Any) -> list[Developer]:
12
+ """Creates Developer objects based on DeveloperFactory configuration.
13
+
14
+ Args:
15
+ count (int): number of developers to create
16
+ **kwargs (Any): Kwargs.
17
+ """
18
+
19
+ if count <= 0:
20
+ raise ValueError("count must be > 0")
21
+
22
+ developers: list[Developer] = []
23
+
24
+ for i in range(count):
25
+ args = cls._generate_args(**kwargs)
26
+
27
+ developers.append(Developer(name=cls._developer_name(i), **args))
28
+
29
+ return developers
30
+
31
+ @classmethod
32
+ def _developer_name(cls, index: int) -> str:
33
+ return f"Developer {index}"
@@ -0,0 +1,43 @@
1
+ from typing import Any, Generator
2
+ import numpy as np
3
+
4
+
5
+ class Factory:
6
+ """Provides shared helpers for creating configured simulation objects."""
7
+
8
+ _rng = np.random.default_rng()
9
+
10
+ @classmethod
11
+ def uniform(cls, low: float, high: float) -> Generator[float, Any, None]:
12
+ """Yield samples from a uniform distribution.
13
+
14
+ Args:
15
+ low (float): Lower bound of the range.
16
+ high (float): Upper bound of the range.
17
+
18
+ Yields:
19
+ float: Successive random samples.
20
+ """
21
+
22
+ while True:
23
+ yield cls._rng.uniform(low, high)
24
+
25
+ @classmethod
26
+ def _generate_args(cls, **kwargs: Any) -> dict[str, Any]:
27
+ """Resolve generator-valued arguments to their next values.
28
+
29
+ Args:
30
+ **kwargs (Any): Values to pass to a factory-created object.
31
+
32
+ Returns:
33
+ dict[str, Any]: Arguments with generator values sampled once.
34
+ """
35
+ args = {}
36
+
37
+ for k, v in kwargs.items():
38
+ if isinstance(v, Generator):
39
+ args[k] = next(v)
40
+ else:
41
+ args[k] = v
42
+
43
+ return args
@@ -0,0 +1,64 @@
1
+ from typing import Any, Type, Optional
2
+ import random
3
+
4
+ from simpy import Environment
5
+
6
+ from value_stream.task import Task
7
+
8
+ from .factory import Factory
9
+
10
+
11
+ class TaskFactory(Factory):
12
+ """Utility for creating Tasks"""
13
+
14
+ def __init__(self, cls: Type[Task] = Task, **task_kwargs: Any) -> None:
15
+ """Initializes a TaskFactory.
16
+
17
+ Args:
18
+ **task_kwargs (Any): Default task constructor arguments.
19
+ """
20
+
21
+ self._task_kwargs = task_kwargs
22
+ self.cls = cls
23
+
24
+ def create(
25
+ self, count: int, env: Optional[Environment] = None, shuffle: bool = True
26
+ ) -> list[Task]:
27
+ """Creates Task objects based on TaskFactory configuration.
28
+
29
+ Args:
30
+ count (int): Number of objects to create.
31
+ env (Optional[Environment]): Simulation environment.
32
+ shuffle (bool): Whether to randomize the created task order.
33
+
34
+ Raises:
35
+ ValueError: If count is not positive or environment time conflicts with creation_sim_t.
36
+
37
+ Returns:
38
+ list[Task]: Tasks created with this factory configuration.
39
+ """
40
+
41
+ if count <= 0:
42
+ raise ValueError("count must be > 0")
43
+
44
+ if (env is not None) and ("creation_sim_t" in self._task_kwargs):
45
+ raise ValueError("env and creation_sim_t are mutually exclusive")
46
+
47
+ tasks: list[Task] = []
48
+
49
+ for i in range(count):
50
+
51
+ args = self._generate_args(**self._task_kwargs)
52
+
53
+ if "task_name" not in args:
54
+ args["task_name"] = f"{i+1}"
55
+
56
+ if env is not None:
57
+ args["creation_sim_t"] = env.now
58
+
59
+ tasks.append(self.cls(**args))
60
+
61
+ if shuffle is True:
62
+ random.shuffle(tasks)
63
+
64
+ return tasks
@@ -0,0 +1,102 @@
1
+ from typing import Optional
2
+
3
+ from simpy import Environment, Interrupt, Process, Store
4
+ from value_stream.task import Task
5
+ from .task_factory import TaskFactory
6
+
7
+
8
+ class TaskGenerator:
9
+ """creates a set of Tasks at a regular simulation time interval.
10
+ Useful for simulating task creation over time and the creation of unexpected toil.
11
+ """
12
+
13
+ def __init__(
14
+ self, factory: TaskFactory, group_size: int = 1, limit: Optional[int] = None
15
+ ) -> None:
16
+ """Configure task generation and its output limit.
17
+
18
+ Args:
19
+ factory (TaskFactory): Task factory used to create tasks.
20
+ group_size (int): Number of tasks generated in each group.
21
+ limit (Optional[int]): Maximum number of items allowed.
22
+ """
23
+
24
+ self.group_size = group_size
25
+ self.factory = factory
26
+
27
+ self.proc: Optional[Process] = None
28
+ self._batch_num = 0
29
+ self.limit = limit
30
+
31
+ def _get_tasks(self, env: Environment) -> list[Task]:
32
+ """Generate the next group of tasks.
33
+
34
+ Args:
35
+ env (Environment): Simulation environment.
36
+
37
+ Returns:
38
+ list[Task]: The generated task group.
39
+ """
40
+
41
+ self._batch_num += 1
42
+ serial_num = 0
43
+
44
+ tasks = self.factory.create(env=env, count=self.group_size)
45
+
46
+ for task in tasks:
47
+ if self.group_size == 1:
48
+ task.task_name = f"S{self._batch_num}"
49
+ else:
50
+ serial_num += 1
51
+ task.task_name = f"S{self._batch_num}-{serial_num}"
52
+
53
+ return tasks
54
+
55
+ def start(self, env: Environment, target: Store, interval: float) -> None:
56
+ """Start generating tasks at the requested interval.
57
+
58
+ Args:
59
+ env (Environment): Simulation environment.
60
+ target (Store): Target.
61
+ interval (float): Interval.
62
+
63
+ Raises:
64
+ ValueError: If the generation interval is not positive.
65
+
66
+ """
67
+
68
+ if interval <= 0:
69
+ raise ValueError("interval must be > 0")
70
+
71
+ self._batch_num = 0
72
+
73
+ def gen(limit: Optional[int]):
74
+
75
+ while True:
76
+
77
+ if limit is not None:
78
+ if limit == 0:
79
+ break
80
+
81
+ limit -= 1
82
+
83
+ try:
84
+
85
+ event = env.timeout(delay=interval)
86
+ yield event
87
+
88
+ value: list[Task] = self._get_tasks(env)
89
+
90
+ for v in value:
91
+ yield target.put(v)
92
+
93
+ except Interrupt:
94
+ break
95
+
96
+ self.proc = env.process(gen(self.limit))
97
+
98
+ def stop(self) -> None:
99
+ """Stop the running operation."""
100
+
101
+ if self.proc is not None:
102
+ self.proc.interrupt()
@@ -0,0 +1,3 @@
1
+ from .simulation_policy import SimulationPolicy
2
+
3
+ __all__ = ["SimulationPolicy"]
@@ -0,0 +1,20 @@
1
+ from abc import abstractmethod
2
+ from typing import Literal
3
+
4
+ from value_stream.resources import ResourcePolicy
5
+ from value_stream.task import Task
6
+ from value_stream.workflow import WorkflowPolicy, AssignmentStrategy
7
+
8
+
9
+ class SimulationPolicy(ResourcePolicy, WorkflowPolicy):
10
+ """encapsulates control logic for a simulation"""
11
+
12
+ @abstractmethod
13
+ def task_priority(
14
+ self, tasks_1: list[Task], tasks_2: list[Task]
15
+ ) -> Literal[0] | Literal[1] | Literal[-1]:
16
+ pass
17
+
18
+ @abstractmethod
19
+ def support_assignment_strategy(self, tasks: list[Task]) -> AssignmentStrategy:
20
+ pass
@@ -0,0 +1,23 @@
1
+ from .resource import Resource
2
+ from .resource_metadata import ResourceMetadata
3
+ from .resource_tracker import InMemoryResourceTracker, NoOpResourceTracker, ResourceTracker
4
+ from .resource_policy import ResourcePolicy
5
+ from .resource_pool import ResourcePool
6
+ from .resource_pool import PooledResource
7
+ from .developer import Developer
8
+ from .qa_tester import QATester
9
+ from .toolchain import Toolchain
10
+
11
+ __all__ = [
12
+ "Resource",
13
+ "InMemoryResourceTracker",
14
+ "Developer",
15
+ "NoOpResourceTracker",
16
+ "QATester",
17
+ "PooledResource",
18
+ "ResourcePolicy",
19
+ "ResourcePool",
20
+ "ResourceTracker",
21
+ "Toolchain",
22
+ "ResourceMetadata",
23
+ ]
@@ -0,0 +1,53 @@
1
+ from typing import Any, Generator
2
+
3
+ from pydantic import Field, FiniteFloat
4
+ from simpy import Environment, Interrupt, Timeout
5
+
6
+ from value_stream.task import EventStatus, Task
7
+
8
+ from .resource import Resource
9
+
10
+
11
+ class Developer(Resource):
12
+ """Simulates actions perform on a task by a software developer"""
13
+
14
+ efficiency: FiniteFloat = Field(default=1.0, gt=0)
15
+ name: str = ""
16
+
17
+ def effort(self, tasks: list[Task]) -> float:
18
+ """Return the developer effort for the supplied tasks.
19
+
20
+ Args:
21
+ tasks (list[Task]): Tasks to process.
22
+
23
+ Returns:
24
+ float: The developer effort assigned to the supplied tasks.
25
+ """
26
+ return sum([task.remaining_work() / self.efficiency for task in tasks])
27
+
28
+ def do_work(self, env: Environment, tasks: list[Task]) -> Generator[Timeout, Any, None]:
29
+ """Apply developer effort to tasks and yield until the work completes.
30
+
31
+ Args:
32
+ env (Environment): Simulation environment.
33
+ tasks (list[Task]): Tasks to process.
34
+
35
+ Yields:
36
+ Timeout: Event that completes developer work.
37
+ """
38
+ start = env.now
39
+
40
+ try:
41
+ yield env.timeout(self.effort(tasks), value={"result": EventStatus.SUCCESS})
42
+
43
+ except Interrupt:
44
+ applied_story_points = (env.now - start) * self.efficiency
45
+ for task in tasks:
46
+ applied_story_points = task.do_work(applied_story_points)
47
+
48
+ raise
49
+
50
+ applied_story_points = (env.now - start) * self.efficiency
51
+
52
+ for task in tasks:
53
+ applied_story_points = task.do_work(applied_story_points)
@@ -0,0 +1,51 @@
1
+ from typing import Any, Generator
2
+ import random
3
+ from pydantic import FiniteFloat, Field
4
+ from simpy import Environment, Timeout
5
+
6
+ from value_stream.task import EventStatus, Task
7
+
8
+ from .resource import Resource
9
+ from .resource_pool import PooledResource
10
+
11
+
12
+ class QATester(Resource, PooledResource):
13
+ """Tests completed development tasks and reports pass or failure."""
14
+
15
+ time_cost: FiniteFloat = Field(default=0.1)
16
+ failure_rate: FiniteFloat = Field(default=0.0, ge=0, le=1)
17
+ failure_cost: FiniteFloat = Field(default=0.0, ge=0, le=1)
18
+
19
+ def do_work(
20
+ self, env: Environment, tasks: list[Task]
21
+ ) -> Generator[Timeout, Any, dict[str, EventStatus]]:
22
+ """Test tasks and yield until the QA work completes.
23
+
24
+ Args:
25
+ env (Environment): Simulation environment.
26
+ tasks (list[Task]): Tasks to process.
27
+
28
+ Yields:
29
+ Timeout: Event that completes QA work.
30
+
31
+ Returns:
32
+ dict[str, EventStatus]: Work result and completion status.
33
+ """
34
+ effort = sum(task.story_points * self.time_cost for task in tasks)
35
+
36
+ if random.random() < self.failure_rate:
37
+ result = EventStatus.FAILURE
38
+ else:
39
+ result = EventStatus.SUCCESS
40
+
41
+ work = env.timeout(effort, value={"result": result})
42
+
43
+ yield work
44
+
45
+ # Cost of remediation is a perceentage of original effort
46
+ if result == EventStatus.FAILURE:
47
+ for task in tasks:
48
+ remediation_work = task.story_points * self.failure_cost
49
+ task.do_work(-remediation_work)
50
+
51
+ return work.value
@@ -0,0 +1,162 @@
1
+ # Parent class for Developer and Toolchain
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Generator, Optional, ClassVar
4
+ import uuid
5
+
6
+ from pydantic import BaseModel, PrivateAttr
7
+ from simpy import Environment, Event, Interrupt, Process, Timeout
8
+ from simpy.events import ProcessGenerator
9
+
10
+ from value_stream.task import TaskState
11
+ from value_stream.task import EventStatus, Task, TaskRouter
12
+
13
+ from .resource_policy import ResourcePolicy
14
+ from .resource_tracker import ResourceTracker
15
+
16
+
17
+ class Resource(ABC, BaseModel):
18
+ """Base class for simulation objects that operate on tasks"""
19
+
20
+ class ProcessWrapper(Process):
21
+ """SimPy process that retains the tasks being processed."""
22
+
23
+ def __init__(self, env: Environment, tasks: list[Task], generator: ProcessGenerator):
24
+ super().__init__(env, generator)
25
+ self.tasks = tasks
26
+
27
+ MAX_BACKLOG: ClassVar[int] = 1000
28
+
29
+ _idle_t: float = 0
30
+ _id: uuid.UUID = PrivateAttr(default_factory=uuid.uuid4)
31
+ _process: Optional[ProcessWrapper] = None
32
+ _suspended_work: list[Event] = []
33
+
34
+ def __hash__(self) -> int:
35
+ return self._id.__hash__()
36
+
37
+ @property
38
+ def resource_id(self) -> uuid.UUID:
39
+ return self._id
40
+
41
+ @property
42
+ def idle_t(self):
43
+ return self._idle_t
44
+
45
+ @idle_t.setter
46
+ def idle_t(self, idle_t: float):
47
+ self._idle_t = idle_t
48
+
49
+ def operate(
50
+ self,
51
+ env: Environment,
52
+ tasks: list[Task],
53
+ task_router: TaskRouter,
54
+ policy: ResourcePolicy,
55
+ workflow_state: TaskState,
56
+ tracker: ResourceTracker,
57
+ ) -> Generator[Event, Any, None]:
58
+ """Simulates an action on a task object.
59
+
60
+ Args:
61
+ env (Environment): Simulation environment.
62
+ tasks (list[Task]): Tasks to process.
63
+ task_router (TaskRouter): Router for completed tasks.
64
+ policy (ResourcePolicy): Policy used to make simulation decisions.
65
+ workflow_state (TaskState): Workflow state associated with the operation.
66
+ tracker (ResourceTracker): Tracker for resource activity.
67
+
68
+ Yields:
69
+ Event: SimPy events yielded while processing tasks.
70
+ """
71
+
72
+ for task in tasks:
73
+ task.start(env.now, workflow_state, resource_id=self._id)
74
+
75
+ if (self._process is not None) and (not self._process.processed):
76
+ if policy.task_priority(tasks, self._process.tasks) == -1:
77
+ self._process.interrupt()
78
+ else:
79
+ # if what's in progress is of higher priority, queue up current work
80
+ # yield self._process ....
81
+ yield env.process(self._pause(env))
82
+
83
+ while True:
84
+ status = EventStatus.SUCCESS
85
+ self._process = self._create_process(env, tasks)
86
+
87
+ start_t = env.now
88
+ try:
89
+
90
+ tracker.start_work(workflow_state, env.now - self._idle_t)
91
+ yield self._process
92
+
93
+ if self._process.value is not None:
94
+ status = self._process.value["result"]
95
+
96
+ except Interrupt:
97
+
98
+ # if do_work is interrupted, wait on a signal that will
99
+ # be triggered once this resource has processed all
100
+ # subsequent interruptions
101
+
102
+ interruption_start_t = env.now
103
+
104
+ tracker.complete_work(workflow_state, status, elapsed_t=env.now - start_t)
105
+
106
+ yield env.process(self._pause(env))
107
+
108
+ tracker.interruption(workflow_state, elapsed_t=env.now - interruption_start_t)
109
+
110
+ continue
111
+
112
+ tracker.complete_work(workflow_state, status, elapsed_t=env.now - start_t)
113
+ self._idle_t = env.now
114
+
115
+ for task in tasks:
116
+ task.end(env.now, workflow_state, status=status, resource_id=self._id)
117
+
118
+ if status == EventStatus.FAILURE:
119
+ yield task_router.route(task=task.as_rework(), status=status)
120
+ else:
121
+ yield task_router.route(task=task.clear_rework(), status=status)
122
+
123
+ # once work is complete, check for previously interrupted work
124
+ # and trigger resumption
125
+ if self._suspended_work:
126
+ signal = self._suspended_work[-1]
127
+ signal.succeed()
128
+
129
+ break
130
+
131
+ @abstractmethod
132
+ def do_work(self, env: Environment, tasks: list[Task]) -> Generator[Timeout, Any, None]:
133
+ pass
134
+
135
+ def _pause(self, env: Environment) -> Generator[Event, Any, None]:
136
+ """Wait until interrupted work can resume.
137
+
138
+ Args:
139
+ env (Environment): Simulation environment.
140
+
141
+ Raises:
142
+ RuntimeError: If the number of suspended work signals exceeds the backlog limit.
143
+
144
+ Yields:
145
+ Event: SimPy events yielded while waiting for work to resume.
146
+ """
147
+ while True:
148
+ signal = env.event()
149
+ self._suspended_work.append(signal)
150
+
151
+ if len(self._suspended_work) > self.MAX_BACKLOG:
152
+ raise RuntimeError("Runaway simulation detected and aborted")
153
+
154
+ yield signal
155
+
156
+ self._suspended_work.pop()
157
+
158
+ if (self._process is not None) and (self._process.target is None):
159
+ break
160
+
161
+ def _create_process(self, env: Environment, tasks: list[Task]):
162
+ return Resource.ProcessWrapper(env, tasks, self.do_work(env, tasks))
@@ -0,0 +1,24 @@
1
+ from pydantic import BaseModel, ConfigDict, field_serializer
2
+ from typing import Optional
3
+ from value_stream.task import TaskState
4
+
5
+
6
+ class ResourceMetadata(BaseModel):
7
+ """Stores metadata collected for one simulation resource."""
8
+
9
+ model_config = ConfigDict(use_enum_values=True)
10
+
11
+ time: float
12
+ state: TaskState
13
+ waiting: int = 0
14
+ allocated: int = 0
15
+ active: int = 0
16
+ success_t: Optional[float] = None
17
+ failure_t: Optional[float] = None
18
+ interruption_t: Optional[float] = None
19
+ waiting_t: Optional[float] = None
20
+ idle_t: Optional[float] = None
21
+
22
+ @field_serializer("state")
23
+ def serialize_event(self, state: TaskState):
24
+ return state.value
@@ -0,0 +1,13 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Literal
3
+ from value_stream.task import Task
4
+
5
+
6
+ class ResourcePolicy(ABC):
7
+ """Defines resource-level decisions during task processing."""
8
+
9
+ @abstractmethod
10
+ def task_priority(
11
+ self, tasks_1: list[Task], tasks_2: list[Task]
12
+ ) -> Literal[0] | Literal[1] | Literal[-1]:
13
+ pass
@@ -0,0 +1,39 @@
1
+ from typing import Type, Optional, Self
2
+ import uuid
3
+ from pydantic import BaseModel, Field, PrivateAttr, ConfigDict
4
+
5
+
6
+ class ResourcePool(BaseModel):
7
+ """Generates a fixed or unlimited quantity of a homogeneous resource"""
8
+
9
+ model_config = ConfigDict(extra="allow")
10
+ class_name: Type = Field()
11
+ limit: Optional[int] = Field(default=None, gt=0)
12
+ _i: int = PrivateAttr(default=0, init=False)
13
+
14
+ _pool_id: uuid.UUID = PrivateAttr(default_factory=uuid.uuid4)
15
+
16
+ def __next__(self):
17
+
18
+ if self.limit is None:
19
+ kwargs = {} if not self.model_extra else self.model_extra
20
+ return self.class_name(**kwargs)
21
+
22
+ if self._i < self.limit:
23
+ self._i += 1
24
+ kwargs = {} if not self.model_extra else self.model_extra
25
+ return self.class_name(**kwargs)
26
+
27
+ raise StopIteration
28
+
29
+ def __iter__(self) -> Self: # pyright: ignore[reportIncompatibleMethodOverride]
30
+ self._i = 0
31
+ return self
32
+
33
+
34
+ class PooledResource:
35
+ """Tracks whether a resource is available to the pool."""
36
+
37
+ @classmethod
38
+ def create_pool(cls, limit: Optional[int] = None, **kwargs):
39
+ return ResourcePool(class_name=cls, limit=limit, **kwargs)