go-astty 0.1.0a0__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.
@@ -0,0 +1,19 @@
1
+ # Environments
2
+ .venv/
3
+ env/
4
+ venv/
5
+
6
+ # Python Cache
7
+ **/__pycache__/
8
+ *.pyc
9
+ *.pyo
10
+ *.pyd
11
+
12
+ # Build and Distribution
13
+ build/
14
+ dist/
15
+ *.egg-info/
16
+
17
+ # Linux System Files
18
+ .DS_Store
19
+ .directory
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fyllus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: go-astty
3
+ Version: 0.1.0a0
4
+ Summary: go-astty is modular process orchestration gate for seamless synchronous and asynchronous command-line executions in Python.
5
+ Project-URL: Codeberg, https://codeberg.org/Fyllus/go-astty
6
+ Project-URL: Github, https://github.com/Fyllus/go-astty
7
+ Author-email: Fyllus <Fyllus@git.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: pygit2>=1.14.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # go-astty
22
+
23
+ `go-astty` or `Gate of Asyncronous and Syncronous TTY`, is a modular process orchestration gate for seamless synchronous and asynchronous command-line executions in Python.
24
+
25
+ `go-astty` decouples process lifecycle management from standard I/O streams by introducing structured, data-driven pipelines. By abstracting execution payload contexts into self-contained Object vectors, it shifts execution responsibility from external engine hooks directly into individual Task runtimes.
26
+
27
+ ## Architecture
28
+
29
+ The framework splits execution into two unified, object-oriented components:
30
+
31
+ * **Tasks (`_BaseTask`)**: Extended list containers acting as payload data vectors. They isolate the executable binary context, handle target system constraints via explicit pre-runtime structural gates (`validation`), and natively invoke their own execution cycles via `.run()`.
32
+ * **Pipers (`_BasePiper`)**: Isolated state engines tied directly to specific task envelopes to capture and track operational boundaries (`stdin`, `stdout`, `stderr`, paths, and exit return codes).
33
+
34
+ ---
35
+
36
+ ## Features
37
+
38
+ * **Self-Contained Runtimes**: Tasks are no longer passive configuration blocks passed to functional routines; execution logic is encapsulated directly within the task objects (`task.run()`).
39
+ * **Pre-Runtime Validation Gates**: Safe assertion tracks (`shutil.which`) evaluate process structure and binary integrity before booting processes to enforce immediate fail-fast mechanics.
40
+ * **Dual Object Engine Layout**: Mirrored execution architectures separating blocking synchronous behaviors (`syncrun.SyncTask`) and non-blocking asynchronous event routines (`asyncrun.AsyncTask`) cleanly under a predictable interface.
41
+
42
+ ---
43
+
44
+ ## Installation
45
+
46
+ To install directly from the source repository:
47
+
48
+ **Clone from Codeberg:**
49
+
50
+ ```bash
51
+ git clone https://codeberg.org/Fyllus/go-astty.git
52
+
53
+ ```
54
+
55
+ **Clone from GitHub:**
56
+
57
+ ```bash
58
+ git clone https://github.com/fyllus/go-astty.git
59
+
60
+ ```
61
+
62
+ **Install:**
63
+
64
+ ```bash
65
+ cd goastty
66
+ pip install .
67
+
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Usage Guide
73
+
74
+ ### 1. Asynchronous Execution Pipeline
75
+
76
+ Perfect for long-running CLI integrations, microservices, or concurrent network-bound stream tracking.
77
+
78
+ ```python
79
+ import asyncio
80
+ from pathlib import Path
81
+ from goastty import asyncrun
82
+
83
+ async def main():
84
+ # Instantiate asynchronous task with payload arguments
85
+ task = asyncrun.AsyncTask("ping", "-c", "5", "google.com")
86
+ task.piper.path = Path.home()
87
+
88
+ # Fire the self-contained non-blocking runtime
89
+ await task.run()
90
+
91
+ # Evaluate context matrices safely
92
+ if task.piper.returncode == 0:
93
+ print(task.piper.stdout.decode("utf-8"))
94
+ else:
95
+ print(f"Error: {task.piper.stderr.decode('utf-8')}")
96
+
97
+ if __name__ == "__main__":
98
+ asyncio.run(main())
99
+
100
+ ```
101
+
102
+ ### 2. Synchronous Execution Pipeline
103
+
104
+ Ideal for local scripts, standard automation sequences, or linear operational pipelines.
105
+
106
+ ```python
107
+ from pathlib import Path
108
+ from goastty import syncrun
109
+
110
+ def run_backup():
111
+ # Build standard array configuration payload
112
+ task = syncrun.SyncTask("tar", "-czf", "backup.tar.gz", "src/")
113
+ task.piper.path = Path.cwd()
114
+
115
+ # Invoke execution directly from the task payload instance
116
+ task.run()
117
+
118
+ print(f"Process finalized with code: {task.piper.returncode}")
119
+
120
+ if __name__ == "__main__":
121
+ run_backup()
122
+
123
+ ```
124
+
125
+ ---
126
+
127
+ ## API Specification
128
+
129
+ ### Core Classes
130
+
131
+ #### `_BasePiper`
132
+
133
+ The logical data matrix tracking standard streams and execution boundaries.
134
+
135
+ * `stdout` / `stderr`: Automatic validation and mutation of incremental stream chunks (`bytearray`).
136
+ * `returncode`: Tracking vector for process termination status.
137
+ * `path`: Explicit execution context location directory (`pathlib.Path`).
138
+ * `shell`: Evaluates whether execution requires a target environment shell gateway.
139
+
140
+ #### `_BaseTask(list)`
141
+
142
+ An extended list structure executing process payload vectors.
143
+
144
+ * `prog`: Tracks the execution binary anchor context (`self[0]`).
145
+ * `args`: Slices away argument payloads safely (`self[1:]`).
146
+ * `validation()`: Evaluates process layout constraints and structural target command existence before booting.
147
+ * `run(stdin=None, kwargs)`: Abstract gateway implemented by runtime engines to drive process setups natively.
@@ -0,0 +1,127 @@
1
+ # go-astty
2
+
3
+ `go-astty` or `Gate of Asyncronous and Syncronous TTY`, is a modular process orchestration gate for seamless synchronous and asynchronous command-line executions in Python.
4
+
5
+ `go-astty` decouples process lifecycle management from standard I/O streams by introducing structured, data-driven pipelines. By abstracting execution payload contexts into self-contained Object vectors, it shifts execution responsibility from external engine hooks directly into individual Task runtimes.
6
+
7
+ ## Architecture
8
+
9
+ The framework splits execution into two unified, object-oriented components:
10
+
11
+ * **Tasks (`_BaseTask`)**: Extended list containers acting as payload data vectors. They isolate the executable binary context, handle target system constraints via explicit pre-runtime structural gates (`validation`), and natively invoke their own execution cycles via `.run()`.
12
+ * **Pipers (`_BasePiper`)**: Isolated state engines tied directly to specific task envelopes to capture and track operational boundaries (`stdin`, `stdout`, `stderr`, paths, and exit return codes).
13
+
14
+ ---
15
+
16
+ ## Features
17
+
18
+ * **Self-Contained Runtimes**: Tasks are no longer passive configuration blocks passed to functional routines; execution logic is encapsulated directly within the task objects (`task.run()`).
19
+ * **Pre-Runtime Validation Gates**: Safe assertion tracks (`shutil.which`) evaluate process structure and binary integrity before booting processes to enforce immediate fail-fast mechanics.
20
+ * **Dual Object Engine Layout**: Mirrored execution architectures separating blocking synchronous behaviors (`syncrun.SyncTask`) and non-blocking asynchronous event routines (`asyncrun.AsyncTask`) cleanly under a predictable interface.
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ To install directly from the source repository:
27
+
28
+ **Clone from Codeberg:**
29
+
30
+ ```bash
31
+ git clone https://codeberg.org/Fyllus/go-astty.git
32
+
33
+ ```
34
+
35
+ **Clone from GitHub:**
36
+
37
+ ```bash
38
+ git clone https://github.com/fyllus/go-astty.git
39
+
40
+ ```
41
+
42
+ **Install:**
43
+
44
+ ```bash
45
+ cd goastty
46
+ pip install .
47
+
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Usage Guide
53
+
54
+ ### 1. Asynchronous Execution Pipeline
55
+
56
+ Perfect for long-running CLI integrations, microservices, or concurrent network-bound stream tracking.
57
+
58
+ ```python
59
+ import asyncio
60
+ from pathlib import Path
61
+ from goastty import asyncrun
62
+
63
+ async def main():
64
+ # Instantiate asynchronous task with payload arguments
65
+ task = asyncrun.AsyncTask("ping", "-c", "5", "google.com")
66
+ task.piper.path = Path.home()
67
+
68
+ # Fire the self-contained non-blocking runtime
69
+ await task.run()
70
+
71
+ # Evaluate context matrices safely
72
+ if task.piper.returncode == 0:
73
+ print(task.piper.stdout.decode("utf-8"))
74
+ else:
75
+ print(f"Error: {task.piper.stderr.decode('utf-8')}")
76
+
77
+ if __name__ == "__main__":
78
+ asyncio.run(main())
79
+
80
+ ```
81
+
82
+ ### 2. Synchronous Execution Pipeline
83
+
84
+ Ideal for local scripts, standard automation sequences, or linear operational pipelines.
85
+
86
+ ```python
87
+ from pathlib import Path
88
+ from goastty import syncrun
89
+
90
+ def run_backup():
91
+ # Build standard array configuration payload
92
+ task = syncrun.SyncTask("tar", "-czf", "backup.tar.gz", "src/")
93
+ task.piper.path = Path.cwd()
94
+
95
+ # Invoke execution directly from the task payload instance
96
+ task.run()
97
+
98
+ print(f"Process finalized with code: {task.piper.returncode}")
99
+
100
+ if __name__ == "__main__":
101
+ run_backup()
102
+
103
+ ```
104
+
105
+ ---
106
+
107
+ ## API Specification
108
+
109
+ ### Core Classes
110
+
111
+ #### `_BasePiper`
112
+
113
+ The logical data matrix tracking standard streams and execution boundaries.
114
+
115
+ * `stdout` / `stderr`: Automatic validation and mutation of incremental stream chunks (`bytearray`).
116
+ * `returncode`: Tracking vector for process termination status.
117
+ * `path`: Explicit execution context location directory (`pathlib.Path`).
118
+ * `shell`: Evaluates whether execution requires a target environment shell gateway.
119
+
120
+ #### `_BaseTask(list)`
121
+
122
+ An extended list structure executing process payload vectors.
123
+
124
+ * `prog`: Tracks the execution binary anchor context (`self[0]`).
125
+ * `args`: Slices away argument payloads safely (`self[1:]`).
126
+ * `validation()`: Evaluates process layout constraints and structural target command existence before booting.
127
+ * `run(stdin=None, kwargs)`: Abstract gateway implemented by runtime engines to drive process setups natively.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "go-astty"
7
+ version = "0.1.0-a"
8
+ description = "go-astty is modular process orchestration gate for seamless synchronous and asynchronous command-line executions in Python."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Fyllus", email = "Fyllus@git.com" }
12
+ ]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+ requires-python = ">=3.11"
23
+ dependencies = [
24
+ "pygit2>=1.14.0",
25
+ ]
26
+ license = { text = "MIT" }
27
+
28
+ [project.urls]
29
+ Codeberg = "https://codeberg.org/Fyllus/go-astty"
30
+ Github = "https://github.com/Fyllus/go-astty"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/goastty"]
@@ -0,0 +1,5 @@
1
+ from . import asyncrun, base, gitfy, syncrun
2
+
3
+ __version__ = '0.1.0-a'
4
+
5
+ __all__ = ['asyncrun', 'base', 'syncrun', 'gitfy']
@@ -0,0 +1,72 @@
1
+ import asyncio
2
+ from asyncio import create_subprocess_exec as async_exec
3
+ from asyncio import create_subprocess_shell as async_shell
4
+ from typing import Any
5
+
6
+ from . import base
7
+
8
+
9
+ class AsyncPiper(base._BasePiper):
10
+ """Asynchronous stream reader and state manager for running sub-processes."""
11
+ def __init__(self) -> None:
12
+ super().__init__()
13
+
14
+ async def stream_stdout(self, stream: asyncio.StreamReader | None) -> None:
15
+ """Consume and append chunks from the asynchronous stdout stream buffer."""
16
+ if not isinstance(stream, asyncio.StreamReader):
17
+ raise TypeError('<stream> must be StreamReader ')
18
+ while True:
19
+ data = await stream.read(4096)
20
+ if not data:
21
+ break
22
+ self.stdout = data
23
+
24
+ async def stream_stderr(self, stream: asyncio.StreamReader | None) -> None:
25
+ """Consume and append chunks from the asynchronous stderr stream buffer."""
26
+ if not isinstance(stream, asyncio.StreamReader):
27
+ raise TypeError('<stream> must be StreamReader ')
28
+ while True:
29
+ data = await stream.read(4096)
30
+ if not data:
31
+ break
32
+ self.stderr = data
33
+
34
+
35
+ class AsyncTask(base._BaseTask):
36
+ """Asynchronous executable command vector managing an AsyncPiper context."""
37
+ def __init__(self, *args: str) -> None:
38
+ super().__init__(*args)
39
+
40
+ @property
41
+ def piper(self) -> AsyncPiper:
42
+ """Get or initialize the asynchronous stream pipeline manager instance."""
43
+ if not hasattr(self, '_piper'):
44
+ setattr(self, '_piper', AsyncPiper())
45
+ return getattr(self, '_piper')
46
+
47
+ async def run(self, stdin: Any = None, **kwargs: Any) -> None:
48
+ """Execute an AsyncTask asynchronously using either shell or executive sub-processes."""
49
+ self.validation()
50
+
51
+ if stdin is not None:
52
+ self.piper.stdin_pipe = stdin
53
+
54
+ kwargs.setdefault('stdin', self.piper.stdin_pipe)
55
+ kwargs.setdefault('stdout', self.piper.stdout_pipe)
56
+ kwargs.setdefault('stderr', self.piper.stderr_pipe)
57
+ kwargs.setdefault('cwd', self.piper.path)
58
+
59
+ if self.piper.shell:
60
+ cmd_str = " ".join(self)
61
+ process = await async_shell(cmd_str, **kwargs)
62
+ else:
63
+ process = await async_exec(self.prog, *self.args, **kwargs)
64
+
65
+ await asyncio.gather(
66
+ self.piper.stream_stdout(process.stdout),
67
+ self.piper.stream_stderr(process.stderr)
68
+ )
69
+
70
+ await process.wait()
71
+ if process.returncode is not None:
72
+ self.piper.returncode = process.returncode
@@ -0,0 +1,211 @@
1
+ import shutil
2
+ import subprocess
3
+ from pathlib import Path
4
+ from typing import Any, Callable, List, TypeVar
5
+
6
+ PIPE = subprocess.PIPE
7
+ STDOUT = subprocess.STDOUT
8
+ DEVNULL = subprocess.DEVNULL
9
+
10
+
11
+ class AssignmentError(Exception):
12
+ """Exception raised for invalid property or attribute assignments."""
13
+ def __init__(self, case: str, obj: object, *expected: type) -> None:
14
+ expected_types = ", ".join(f"<{t.__name__}>" for t in expected)
15
+ self._cases = {
16
+ 'none': f'Property/Attribute cannot be None: expected {expected_types}, got <{type(obj).__name__}>',
17
+ 'unable': f'Unable to assign type <{type(obj).__name__}>: expected {expected_types}',
18
+ 'invalid': f'Invalid Assignment: expected {expected_types}, cannot use <{type(obj).__name__}>'
19
+ }
20
+ super().__init__(self._cases.get(case, f"Unknown error case with type <{type(obj).__name__}>"))
21
+
22
+
23
+ class TaskError(Exception):
24
+ """Exception raised for validation failures before task runtime execution."""
25
+ def __init__(self, flag: str, task: "_BaseTask") -> None:
26
+ self._cases = {
27
+ 'empty': f'Unable to run empty task: {task}',
28
+ 'invalid_command': f'Not found or unknown command {task.prog}'
29
+ }
30
+ super().__init__(self._cases.get(flag, f'Unknown error case with task {task}'))
31
+
32
+
33
+ BaseTask = TypeVar('BaseTask', bound='_BaseTask')
34
+ BasePiper = TypeVar('BasePiper', bound='_BasePiper')
35
+
36
+
37
+ class _BasePiper:
38
+ """Manage stream pipes, exit status, and execution context for a process."""
39
+ def __init__(self) -> None:
40
+ pass
41
+
42
+ def __getitem__(self, name: str) -> Any:
43
+ return getattr(self, name)
44
+
45
+ def __setitem__(self, name: str, value: Any) -> None:
46
+ if not hasattr(self, name):
47
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
48
+ setattr(self, name, value)
49
+
50
+ # ---------- class property -------------------
51
+ @property
52
+ def stdout(self) -> bytearray:
53
+ """Get the accumulated standard output bytes."""
54
+ if not hasattr(self, '_stdout'):
55
+ setattr(self, '_stdout', bytearray())
56
+ return getattr(self, '_stdout')
57
+
58
+ @stdout.setter
59
+ def stdout(self, value: bytes | bytearray) -> None:
60
+ if not isinstance(value, (bytes, bytearray)):
61
+ raise AssignmentError('invalid', value, bytes, bytearray)
62
+ self.stdout.extend(value)
63
+
64
+ @property
65
+ def stderr(self) -> bytearray:
66
+ """Get the accumulated standard error bytes."""
67
+ if not hasattr(self, '_stderr'):
68
+ setattr(self, '_stderr', bytearray())
69
+ return getattr(self, '_stderr')
70
+
71
+ @stderr.setter
72
+ def stderr(self, value: bytes | bytearray) -> None:
73
+ if not isinstance(value, (bytes, bytearray)):
74
+ raise AssignmentError('invalid', value, bytes, bytearray)
75
+ self.stderr.extend(value)
76
+
77
+ @property
78
+ def returncode(self) -> int | None:
79
+ """Get the process exit code integer after termination."""
80
+ if not hasattr(self, '_returncode'):
81
+ setattr(self, '_returncode', None)
82
+ return getattr(self, '_returncode')
83
+
84
+ @returncode.setter
85
+ def returncode(self, value: int) -> None:
86
+ if not isinstance(value, int):
87
+ raise AssignmentError('unable', value, int)
88
+ self._returncode = value
89
+
90
+ @property
91
+ def stdout_pipe(self) -> int:
92
+ """Get the internal target destination descriptor for stdout."""
93
+ if not hasattr(self, '_stdout_pipe'):
94
+ setattr(self, '_stdout_pipe', PIPE)
95
+ return getattr(self, '_stdout_pipe')
96
+
97
+ @property
98
+ def stderr_pipe(self) -> int:
99
+ """Get the internal target destination descriptor for stderr."""
100
+ if not hasattr(self, '_stderr_pipe'):
101
+ setattr(self, '_stderr_pipe', PIPE)
102
+ return getattr(self, '_stderr_pipe')
103
+
104
+ @property
105
+ def stdin_pipe(self) -> Any:
106
+ """Get the source input stream pipeline anchor for stdin."""
107
+ if not hasattr(self, '_stdin'):
108
+ setattr(self, '_stdin', None)
109
+ return getattr(self, '_stdin')
110
+
111
+ @stdin_pipe.setter
112
+ def stdin_pipe(self, value: Any) -> None:
113
+ setattr(self, '_stdin', value)
114
+
115
+ @property
116
+ def path(self) -> Path:
117
+ """Get the filesystem directory context where execution occurs."""
118
+ if not hasattr(self, '_path'):
119
+ setattr(self, '_path', Path.cwd())
120
+ return getattr(self, '_path')
121
+
122
+ @path.setter
123
+ def path(self, value: Path) -> None:
124
+ if value is None:
125
+ raise AssignmentError('none', value, Path)
126
+ if not isinstance(value, Path):
127
+ raise AssignmentError('unable', value, Path)
128
+ setattr(self, '_path', value)
129
+
130
+ @property
131
+ def shell(self) -> bool:
132
+ """Check whether direct system shell execution is enabled."""
133
+ if not hasattr(self, '_shell'):
134
+ setattr(self, '_shell', False)
135
+ return getattr(self, '_shell')
136
+
137
+ @shell.setter
138
+ def shell(self, value: bool) -> None:
139
+ if not isinstance(value, bool):
140
+ raise AssignmentError('unable', value, bool)
141
+ setattr(self, '_shell', value)
142
+
143
+
144
+ class _BaseTask(list):
145
+ """Represent an executable command payload structure as an array of arguments."""
146
+ def __init__(self, *args: str) -> None:
147
+ super().__init__()
148
+ if args:
149
+ self.extend(args)
150
+
151
+ def __bool__(self) -> bool:
152
+ return len(self) > 0
153
+
154
+ # ---------- class property -------------------
155
+ @property
156
+ def prog(self) -> str:
157
+ """Get the root binary filename executable anchor of the command."""
158
+ if not self:
159
+ return ''
160
+ return self[0]
161
+
162
+ @property
163
+ def args(self) -> list:
164
+ """Get the trailing arguments list passed to the executable payload."""
165
+ if not len(self) > 1:
166
+ return []
167
+ return self[1:]
168
+
169
+ @property
170
+ def piper(self) -> Any:
171
+ """Get the stream pipeline manager tied to this specific task execution context."""
172
+ if not hasattr(self, '_piper'):
173
+ setattr(self, '_piper', _BasePiper())
174
+ return getattr(self, '_piper')
175
+
176
+ @piper.setter
177
+ def piper(self, value: Any) -> None:
178
+ if not isinstance(value, _BasePiper):
179
+ raise AssignmentError('unable', value, _BasePiper)
180
+ setattr(self, '_piper', value)
181
+
182
+ @property
183
+ def callback(self) -> Any:
184
+ """Get the execution hook function triggered upon process completion."""
185
+ return getattr(self, '_callback', None)
186
+
187
+ @callback.setter
188
+ def callback(self, value: Any) -> None:
189
+ if not callable(value):
190
+ raise AssignmentError('invalid', value, Callable)
191
+ setattr(self, '_callback', value)
192
+
193
+ # --------------- main methods -------------------------------
194
+ def append(self, value: str) -> None:
195
+ """Append a safe string argument entry to the command vector."""
196
+ if not isinstance(value, str):
197
+ raise ValueError('Value must be <str>')
198
+ super().append(value)
199
+
200
+ def extend(self, value: List[str]) -> None:
201
+ """Extend the command argument payload using an array of string slices."""
202
+ if not all(isinstance(arg_item, str) for arg_item in value):
203
+ raise ValueError('Value must be <List[str]>')
204
+ super().extend(value)
205
+
206
+ def validation(self) -> None:
207
+ """Pre-Runtime validation gate to check structural faults before process boot."""
208
+ if not self:
209
+ raise TaskError('empty', self)
210
+ if not self.piper.shell and not shutil.which(self.prog):
211
+ raise TaskError('invalid_command', self)
@@ -0,0 +1,189 @@
1
+ from pathlib import Path
2
+ from typing import (
3
+ List,
4
+ Optional,
5
+ Self,
6
+ )
7
+
8
+ from pygit2 import (
9
+ Commit,
10
+ GitError,
11
+ Index,
12
+ Repository,
13
+ Signature,
14
+ clone_repository,
15
+ init_repository,
16
+ )
17
+
18
+
19
+ class GitRepoError(Exception):
20
+ def __init__(self, *args: object) -> None:
21
+ super().__init__(*args)
22
+
23
+ def open_repo(repo: Repository | Path) -> Repository:
24
+ """Open an existing git repository from a Path or Repository object."""
25
+ if isinstance(repo, Repository):
26
+ return repo
27
+ if not isinstance(repo, Path):
28
+ raise GitRepoError(f'Invalid repository type <{type(repo).__name__}>, expected <RepositoryOrPath>')
29
+ try:
30
+ return Repository(str(repo))
31
+ except (KeyError, GitError):
32
+ raise GitRepoError('Not a valid repository inited.')
33
+
34
+ def is_git(path: Path) -> bool:
35
+ """Check if the given path is a valid git repository."""
36
+ try:
37
+ open_repo(repo=path)
38
+ return True
39
+ except GitRepoError:
40
+ return False
41
+
42
+ class GitRepo:
43
+ """
44
+ TODO: log, status, tag
45
+ """
46
+ def __init__(self, repo: Repository | Path) -> None:
47
+ self.repo = repo
48
+
49
+ # ---------------------- class property ---------------------------
50
+ @property
51
+ def index(self) -> Index:
52
+ """Get the repository index (staging area)."""
53
+ return self.repo.index
54
+
55
+ @property
56
+ def repo(self) -> Repository:
57
+ """Get the underlying native repository instance."""
58
+ if not hasattr(self, '_repo'):
59
+ setattr(self, '_repo', None)
60
+ return getattr(self, '_repo')
61
+
62
+ @property
63
+ def workdir(self) -> Path:
64
+ """Get the absolute path to the working directory."""
65
+ return Path(self.repo.workdir).resolve()
66
+
67
+ @repo.setter
68
+ def repo(self, repo: Repository | Path) -> None:
69
+ new_repo = open_repo(repo=repo)
70
+ setattr(self, '_repo', new_repo)
71
+
72
+ # ---------------- repo methods ------------------
73
+ @classmethod
74
+ def init(cls, path: Path, bare: bool = False, **kwargs) -> "GitRepo":
75
+ """Initialize a new git repository or open it if already initialized."""
76
+ if is_git(path):
77
+ return cls(open_repo(repo=path))
78
+ if not path.exists():
79
+ raise GitRepoError(f"Path does not exist: invalid try to init a repo at '{path}'.")
80
+ native_repo = init_repository(str(path), bare=bare, **kwargs)
81
+ return cls(native_repo)
82
+
83
+ @classmethod
84
+ def clone(cls, url: str, path: Path, **kwargs) -> "GitRepo":
85
+ """Clone a remote git repository into a local directory."""
86
+ if is_git(path):
87
+ raise GitRepoError(f"Cannot clone: a valid git repository already exists at '{path}'.")
88
+ native_repo = clone_repository(url, str(path), **kwargs)
89
+ return cls(native_repo)
90
+
91
+ # ------------- main methods -----------------------------
92
+ def add(self, *files: Path) -> Self:
93
+ """Stage files or all modifications to the repository index."""
94
+ if len(files) == 0:
95
+ self.index.add_all()
96
+ else:
97
+ for file_path in files:
98
+ abs_path = file_path.resolve()
99
+ try:
100
+ rel_path = abs_path.relative_to(self.workdir)
101
+ self.index.add(str(rel_path))
102
+ except ValueError:
103
+ raise ValueError(
104
+ f"File {abs_path} out of repository {self.workdir}"
105
+ )
106
+ self.index.write()
107
+ return self
108
+
109
+ def remove(self, *files: Path, staged_only: bool = False) -> Self:
110
+ """Remove files from the repository index and optionally from the working directory."""
111
+ for file_path in files:
112
+ rel_path = file_path.resolve().relative_to(self.workdir)
113
+ str_path = str(rel_path)
114
+
115
+ if str_path in self.index:
116
+ self.index.remove(str_path)
117
+
118
+ if not staged_only:
119
+ abs_path = self.workdir / rel_path
120
+ if abs_path.exists():
121
+ abs_path.unlink()
122
+
123
+ self.index.write()
124
+ return self
125
+
126
+ def move(self, source: Path, destination: Path) -> Self:
127
+ """Move or rename a file within the working directory and update the index."""
128
+ abs_src = source.resolve()
129
+ abs_dst = destination.resolve()
130
+
131
+ rel_src = abs_src.relative_to(self.workdir)
132
+ rel_dst = abs_dst.relative_to(self.workdir)
133
+
134
+ if abs_src.exists():
135
+ abs_dst.parent.mkdir(parents=True, exist_ok=True)
136
+ abs_src.rename(abs_dst)
137
+
138
+ str_src = str(rel_src)
139
+ if str_src in self.index:
140
+ self.index.remove(str_src)
141
+
142
+ self.index.add(str(rel_dst))
143
+ self.index.write()
144
+ return self
145
+
146
+ def restore(self, *files: Path, ref: str = 'HEAD') -> Self:
147
+ """Discard local modifications by restoring files from a specific commit reference."""
148
+ try:
149
+ commit = self.repo.revparse_single(ref)
150
+ commit_tree = commit.tree
151
+ except KeyError:
152
+ return self
153
+
154
+ if len(files) == 0:
155
+ self.repo.checkout_tree(commit_tree, strategy=1, directory=str(self.workdir))
156
+ self.index.read()
157
+ else:
158
+ for file_path in files:
159
+ rel_path = file_path.resolve().relative_to(self.workdir)
160
+ str_path = str(rel_path)
161
+
162
+ self.repo.checkout_tree(
163
+ commit_tree,
164
+ strategy=1,
165
+ paths=[str_path],
166
+ directory=str(self.workdir)
167
+ )
168
+
169
+ self.index.read()
170
+ return self
171
+
172
+ def commit(self,
173
+ author: Signature, committer: Signature,
174
+ message: str, parents: Optional[List[Commit]] = None,
175
+ ref: str = 'HEAD'
176
+ ) -> Self:
177
+ """Commit staged changes to the repository history."""
178
+ tree_oid = self.index.write_tree()
179
+ if parents is None:
180
+ parents = []
181
+ if len(parents) == 0:
182
+ try:
183
+ current_tip = self.repo.revparse_single(ref)
184
+ if isinstance(current_tip, Commit):
185
+ parents = [current_tip]
186
+ except KeyError:
187
+ pass
188
+ self.repo.create_commit(ref, author, committer, message, tree_oid, parents)
189
+ return self
@@ -0,0 +1,50 @@
1
+ from subprocess import run as shell
2
+ from typing import Any
3
+
4
+ from . import base
5
+
6
+
7
+ class SyncPiper(base._BasePiper):
8
+ """Synchronous stream reader and state manager for running sub-processes."""
9
+ def __init__(self) -> None:
10
+ super().__init__()
11
+
12
+
13
+ class SyncTask(base._BaseTask):
14
+ """Synchronous executable command vector managing a SyncPiper context."""
15
+ def __init__(self, *args: str) -> None:
16
+ super().__init__(*args)
17
+
18
+ @property
19
+ def piper(self) -> SyncPiper:
20
+ """Get or initialize the synchronous stream pipeline manager instance."""
21
+ if not hasattr(self, '_piper'):
22
+ setattr(self, '_piper', SyncPiper())
23
+ return getattr(self, '_piper')
24
+
25
+ def run(self, stdin: Any = None, **kwargs: Any) -> None:
26
+ """Execute a SyncTask synchronously using either shell or executive sub-processes."""
27
+ self.validation()
28
+
29
+ if stdin is not None:
30
+ self.piper.stdin_pipe = stdin
31
+
32
+ kwargs.setdefault('stdin', self.piper.stdin_pipe)
33
+ kwargs.setdefault('stdout', self.piper.stdout_pipe)
34
+ kwargs.setdefault('stderr', self.piper.stderr_pipe)
35
+ kwargs.setdefault('cwd', self.piper.path)
36
+ kwargs.setdefault('shell', self.piper.shell)
37
+
38
+ if not self.piper.shell:
39
+ cmd = list(self)
40
+ process = shell(cmd, **kwargs)
41
+ else:
42
+ cmd_str = ' '.join(self)
43
+ process = shell(cmd_str, **kwargs)
44
+
45
+ if process.stdout is not None:
46
+ self.piper.stdout = process.stdout
47
+ if process.stderr is not None:
48
+ self.piper.stderr = process.stderr
49
+
50
+ self.piper.returncode = process.returncode
@@ -0,0 +1,29 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+
4
+ from goastty import asyncrun
5
+
6
+
7
+ async def main(c: int = 5) -> None:
8
+ # Safe argument array context for native ping execution
9
+ task = asyncrun.AsyncTask("ping", "-Dv", "-c", str(c), "google.com")
10
+ task.piper.path = Path.home()
11
+
12
+ print({"status": "starting", "command": list(task)})
13
+
14
+ # Dispatch the execution coroutine to the background event loop
15
+ run_task = asyncio.create_task(task.run())
16
+
17
+ # Monitor engine execution while returncode remains unassigned
18
+ while task.piper.returncode is None:
19
+ print("[Async Engine] Awaiting ping response... process actively running.")
20
+ await asyncio.sleep(0.5)
21
+
22
+ await run_task
23
+
24
+ print(f"\n[Async Engine] Process finalized with code: {task.piper.returncode}")
25
+ print(f"Accumulated STDOUT buffer:\n{task.piper.stdout.decode('utf-8', errors='ignore')}")
26
+
27
+
28
+ if __name__ == "__main__":
29
+ asyncio.run(main(5))
@@ -0,0 +1,31 @@
1
+ import threading
2
+ import time
3
+ from pathlib import Path
4
+
5
+ from goastty import syncrun
6
+
7
+
8
+ def main(c: int=5) -> None:
9
+ # Initialize sequential task vector context
10
+ task = syncrun.SyncTask("ping", "-Dv", "-c", str(c), "google.com")
11
+ task.piper.path = Path.home()
12
+
13
+ print({"status": "starting", "command": list(task)})
14
+
15
+ # Dispatch the blocking execution thread to decouple I/O boundary tracking
16
+ worker_thread = threading.Thread(target=task.run)
17
+ worker_thread.start()
18
+
19
+ # Core main loop monitors the state engine without stopping the thread runtime
20
+ while task.piper.returncode is None:
21
+ print("[Sync Engine] Processing execution pipeline... task locked in worker thread.")
22
+ time.sleep(0.5)
23
+
24
+ worker_thread.join()
25
+
26
+ print(f"\n[Sync Engine] Process finalized with code: {task.piper.returncode}")
27
+ print(f"Accumulated STDOUT buffer:\n{task.piper.stdout.decode('utf-8', errors='ignore')}")
28
+
29
+
30
+ if __name__ == "__main__":
31
+ main(5)