gitbolt 0.0.0.dev15__tar.gz → 0.0.0.dev17__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.
Files changed (36) hide show
  1. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/PKG-INFO +30 -4
  2. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/README.md +28 -2
  3. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/pyproject.toml +3 -5
  4. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/base.py +190 -7
  5. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/impl/simple.py +13 -1
  6. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/runner/base.py +34 -1
  7. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/runner/simple.py +49 -2
  8. gitbolt-0.0.0.dev17/src/gitbolt/subprocess/session.py +33 -0
  9. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt.egg-info/PKG-INFO +30 -4
  10. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt.egg-info/SOURCES.txt +1 -0
  11. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt.egg-info/requires.txt +1 -1
  12. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/LICENSE +0 -0
  13. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/setup.cfg +0 -0
  14. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/__init__.py +0 -0
  15. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/_internal_init.py +0 -0
  16. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/add.py +0 -0
  17. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/base.py +0 -0
  18. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/constants.py +0 -0
  19. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/exceptions.py +0 -0
  20. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/ls_tree.py +0 -0
  21. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/models.py +0 -0
  22. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/py.typed +0 -0
  23. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/pytest_plugin.py +0 -0
  24. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/__init__.py +0 -0
  25. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/_internal_init.py +0 -0
  26. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/add.py +0 -0
  27. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/constants.py +0 -0
  28. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/exceptions.py +0 -0
  29. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/impl/__init__.py +0 -0
  30. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/ls_tree.py +0 -0
  31. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/runner/__init__.py +0 -0
  32. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/subprocess/utils.py +0 -0
  33. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt/utils.py +0 -0
  34. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt.egg-info/dependency_links.txt +0 -0
  35. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt.egg-info/entry_points.txt +0 -0
  36. {gitbolt-0.0.0.dev15 → gitbolt-0.0.0.dev17}/src/gitbolt.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitbolt
3
- Version: 0.0.0.dev15
3
+ Version: 0.0.0.dev17
4
4
  Summary: Fast, flexible and type-safe Git commands in Python.
5
5
  Author-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
6
6
  Maintainer-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
@@ -27,7 +27,7 @@ Requires-Python: >=3.12
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
29
  Requires-Dist: vt-err-hndlr>=0.0.0dev6
30
- Requires-Dist: vt-commons>=0.0.1.dev9
30
+ Requires-Dist: vt-commons>=0.0.1.dev11
31
31
  Provides-Extra: pygit2
32
32
  Requires-Dist: pygit2; extra == "pygit2"
33
33
  Provides-Extra: test
@@ -327,10 +327,12 @@ no_advice_reset_git = overridden_git.git_opts_override(no_advice=False)
327
327
 
328
328
  At last, run unchecked commands in git.
329
329
 
330
- Introduced in `0.0.0dev4` to
330
+ Introduced in `0.0.0.dev4` to
331
331
  - experiment.
332
332
  - have consistent interfaced commands run until all subcommands are provided by the library.
333
333
 
334
+ #### 🖥️ Run one process per command
335
+
334
336
  ```python
335
337
  import gitbolt
336
338
 
@@ -340,9 +342,33 @@ git.subcmd_unchecked.run(['--version']) # run the version option for git.
340
342
  git.subcmd_unchecked.run(['version']) # run the version subcommand.
341
343
  ```
342
344
 
345
+ #### 🖥️ Run one long-running process and communicate with it
346
+
347
+ Introduced in `0.0.0.dev16` to:
348
+ - Make communicable processes using `subprocess.Popen`.
349
+
350
+ Get a long-running process and communicate with it for batching and faster operations.
351
+
352
+ ```python
353
+ import gitbolt
354
+ import sys
355
+
356
+ git = gitbolt.get_git_command()
357
+ with git.subcmd_unchecked.popen(["cat-file", "--batch-command"]) as cf:
358
+ cf.stdin.write(b"contents HEAD\n")
359
+ cf.stdin.flush()
360
+ header = cf.stdout.readline().strip()
361
+ print(f"HEADER: {header}", file=sys.stderr)
362
+ obj, typ, size = header.split()
363
+ print(cf.stdout.read(int(size)))
364
+ cf.stdout.readline()
365
+ ```
366
+
367
+ Error handling and I/O management is left to the client/caller.
368
+
343
369
  #### 💻 Run commands received from CLI
344
370
 
345
- Introduced in `0.0.0dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
371
+ Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
346
372
 
347
373
  While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
348
374
  would be to make a system that receives CLI commands and does certain modifications/additions inside `gitbolt` before
@@ -291,10 +291,12 @@ no_advice_reset_git = overridden_git.git_opts_override(no_advice=False)
291
291
 
292
292
  At last, run unchecked commands in git.
293
293
 
294
- Introduced in `0.0.0dev4` to
294
+ Introduced in `0.0.0.dev4` to
295
295
  - experiment.
296
296
  - have consistent interfaced commands run until all subcommands are provided by the library.
297
297
 
298
+ #### 🖥️ Run one process per command
299
+
298
300
  ```python
299
301
  import gitbolt
300
302
 
@@ -304,9 +306,33 @@ git.subcmd_unchecked.run(['--version']) # run the version option for git.
304
306
  git.subcmd_unchecked.run(['version']) # run the version subcommand.
305
307
  ```
306
308
 
309
+ #### 🖥️ Run one long-running process and communicate with it
310
+
311
+ Introduced in `0.0.0.dev16` to:
312
+ - Make communicable processes using `subprocess.Popen`.
313
+
314
+ Get a long-running process and communicate with it for batching and faster operations.
315
+
316
+ ```python
317
+ import gitbolt
318
+ import sys
319
+
320
+ git = gitbolt.get_git_command()
321
+ with git.subcmd_unchecked.popen(["cat-file", "--batch-command"]) as cf:
322
+ cf.stdin.write(b"contents HEAD\n")
323
+ cf.stdin.flush()
324
+ header = cf.stdout.readline().strip()
325
+ print(f"HEADER: {header}", file=sys.stderr)
326
+ obj, typ, size = header.split()
327
+ print(cf.stdout.read(int(size)))
328
+ cf.stdout.readline()
329
+ ```
330
+
331
+ Error handling and I/O management is left to the client/caller.
332
+
307
333
  #### 💻 Run commands received from CLI
308
334
 
309
- Introduced in `0.0.0dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
335
+ Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
310
336
 
311
337
  While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
312
338
  would be to make a system that receives CLI commands and does certain modifications/additions inside `gitbolt` before
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gitbolt"
3
- version = "0.0.0dev15"
3
+ version = "0.0.0.dev17"
4
4
  description = "Fast, flexible and type-safe Git commands in Python."
5
5
  requires-python = ">=3.12"
6
6
  readme = "README.md"
@@ -37,7 +37,7 @@ classifiers = [
37
37
  ]
38
38
  dependencies = [
39
39
  "vt-err-hndlr >= 0.0.0dev6",
40
- "vt-commons >= 0.0.1.dev9",
40
+ "vt-commons >= 0.0.1.dev11",
41
41
  ]
42
42
 
43
43
  [project.optional-dependencies]
@@ -45,9 +45,7 @@ pygit2 = ['pygit2']
45
45
  test = ['pytest']
46
46
 
47
47
  [dependency-groups]
48
- # TODO: report the mypy recent version (1.16.0, 1.16.1 issue) RuntimeError: ('Not implemented', <class 'mypy.types.TypeGuardedType'>)
49
- # btw, this error doesn't show in the latest developer version. So, this error will go away with 1.17.0 relase
50
- dev = ["mypy==1.15.0", "ruff"]
48
+ dev = ["mypy", "ruff"]
51
49
  doc = ['sphinx', 'sphinx-argparse']
52
50
  test = ["pytest", 'pytest-cov', "pytest-xdist"]
53
51
  multitest = ['tox']
@@ -9,8 +9,10 @@ from __future__ import annotations
9
9
 
10
10
  from abc import abstractmethod, ABC
11
11
  from collections.abc import Callable
12
+ from contextlib import AbstractContextManager
12
13
  from pathlib import Path
13
- from subprocess import CompletedProcess
14
+ from subprocess import CompletedProcess, Popen, PIPE
15
+ from types import SimpleNamespace
14
16
  from typing import override, Protocol, Unpack, Self, overload, Literal, Any
15
17
 
16
18
  from vt.utils.commons.commons.core_py import is_unset, not_none_not_unset
@@ -295,9 +297,94 @@ class GitCommand(Git, ABC):
295
297
  def subcmd_unchecked(self) -> UncheckedSubcmd:
296
298
  """
297
299
  Run an unchecked git subcommand using subprocess.
300
+
301
+ :returns: An unchecked subcommand instance that simply runs the asked command in a ``subprocess.run()`` with
302
+ optional ``subprocess.Popen()``.
298
303
  """
299
304
  ...
300
305
 
306
+ @abstractmethod
307
+ def session(self, **commands: list[str] | Callable[[], Popen[bytes]]) -> GitSession:
308
+ """
309
+ Run long-running git session with multiple unchecked subcommands using ``subprocess.Popen`` and
310
+ communicate with them.
311
+
312
+ Client/Caller can communicate with ``GitSession``'s processes with their stdin and stdout.
313
+
314
+ Examples:
315
+
316
+ Obtain a session:
317
+
318
+ >>> import gitbolt
319
+ >>> _git = gitbolt.get_git_command()
320
+ >>> ses = _git.session(ls_tree=["ls-tree", "HEAD"], cat_file=["cat-file", "--batch"])
321
+ >>> with ses: # start the session by ctx mgr
322
+ ... pass # any communication can be done by Popen semantics.
323
+
324
+ Start the session in one go:
325
+
326
+ >>> with _git.session(cat_file=["cat-file", "--batch"]) as ses: # obtain, start and ctx manage the session.
327
+ ... pass # any communication can be done by Popen semantics.
328
+
329
+ :param commands: list of string git commands suppliable to ``subprocess.Popen`` or ``subprocess.Popen`` lambdas.
330
+ :returns: A (not yet started) long-running ``GitSession`` context manager.
331
+ """
332
+ ...
333
+
334
+
335
+ class GitSession(HasGitUnderneath[GitCommand], AbstractContextManager):
336
+
337
+ def __init__(self, git: GitCommand, **commands: Callable[[], Popen[bytes]]):
338
+ """
339
+ Context Manager to start a git long-running session. Useful when a command is to be held in open state and be
340
+ communicated with its stdin and stdout. This is way faster that spawning multiple processes each time.
341
+
342
+ Examples:
343
+
344
+ Obtain a session:
345
+
346
+ >>> import gitbolt
347
+ >>> _git = gitbolt.get_git_command()
348
+ >>> ses = _git.session(ls_tree=["ls-tree", "HEAD"], cat_file=["cat-file", "--batch"])
349
+ >>> with ses: # start the session by ctx mgr
350
+ ... pass # any communication can be done by Popen semantics.
351
+
352
+ Start the session in one go:
353
+
354
+ >>> with _git.session(cat_file=["cat-file", "--batch"]) as ses: # obtain, start and ctx manage the session.
355
+ ... pass # any communication can be done by Popen semantics.
356
+
357
+ :param git: ``gitbolt.subprocess.GitCommand`` instance.
358
+ :param commands: commands in kwargs fashion.
359
+ """
360
+ self._git = git
361
+ self.unstarted_commands: dict[str, Callable[[], Popen[bytes]]] = commands
362
+ self.started_commands: dict[str, Popen[bytes]] = dict()
363
+ self.commands: SimpleNamespace | None = None
364
+
365
+ def __enter__(self) -> Self:
366
+ processes_started_keys: list[str] = []
367
+ for unstarted_command_key, unstarted_command in self.unstarted_commands.items():
368
+ self.started_commands[unstarted_command_key] = unstarted_command().__enter__()
369
+ processes_started_keys.append(unstarted_command_key)
370
+ self.commands = SimpleNamespace(**self.started_commands)
371
+ for pk in processes_started_keys:
372
+ del self.unstarted_commands[pk]
373
+
374
+ @property
375
+ def git(self) -> GitCommand:
376
+ return self._git
377
+
378
+ def __exit__(self, exc_type, exc_value, traceback, /):
379
+ process_done_keys: list[str] = []
380
+ for started_popen_key, started_popen in self.started_commands.items():
381
+ started_popen.__exit__(exc_type, exc_value, traceback)
382
+ process_done_keys.append(started_popen_key)
383
+ self.commands = None
384
+ for pk in process_done_keys:
385
+ del self.started_commands[pk]
386
+ return False
387
+
301
388
 
302
389
  class GitSubcmdCommand(GitSubCommand, HasGitUnderneath["GitCommand"], Protocol):
303
390
  """
@@ -584,12 +671,8 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
584
671
 
585
672
  :return: ``CompletedProcess`` capturing all the required stdout, stderr, return-code etc.
586
673
  """
587
- main_cmd_args = self.git.build_main_cmd_args()
588
- envs_vars = self.git.build_git_envs()
589
- another_supplied_env = subprocess_run_kwargs.pop("env", None)
590
- if another_supplied_env:
591
- if envs_vars is not None:
592
- envs_vars.update(another_supplied_env)
674
+ main_cmd_args = self.git_main_cmd_args()
675
+ envs_vars = self.git_envs(subprocess_run_kwargs.pop("env", None))
593
676
  cwd = subprocess_run_kwargs.pop("cwd", self.root_dir)
594
677
  capture_output = subprocess_run_kwargs.pop("capture_output", True)
595
678
  check = subprocess_run_kwargs.pop("check", True)
@@ -607,3 +690,103 @@ class UncheckedSubcmd(GitSubcmdCommand, RootDirOp, Protocol):
607
690
  **subprocess_run_kwargs,
608
691
  )
609
692
  return result
693
+
694
+ @overload
695
+ def popen(
696
+ self,
697
+ subcommand_args: list[str],
698
+ *popen_args: Any,
699
+ text: Literal[True] = True,
700
+ **popen_kwargs: Any,
701
+ ) -> Popen[str]: ...
702
+
703
+ @overload
704
+ def popen(
705
+ self,
706
+ subcommand_args: list[str],
707
+ *popen_args: Any,
708
+ text: Literal[False] = False,
709
+ **popen_kwargs: Any,
710
+ ) -> Popen[bytes]: ...
711
+
712
+ def popen(
713
+ self,
714
+ subcommand_args: list[str],
715
+ *popen_args: Any,
716
+ text: Literal[True, False] = False,
717
+ **popen_kwargs: Any,
718
+ ) -> Popen[str] | Popen[bytes]:
719
+ """
720
+ Open unchecked git subcommand communicable process, using ``subprocess.Popen``.
721
+
722
+ All the arguments are congruent to ``subprocess.Popen`` and mostly passes as-is.
723
+
724
+ :param subcommand_args: the full subcommand argument list.
725
+ :param popen_args: additional subprocess positionals.
726
+ :param text: ``_input`` and returns both are str if this value is ``True``. Else, bytes are considered.
727
+ :param popen_kwargs: additional subprocess keyword arguments.
728
+
729
+ :return: ``Popen`` capturing all the required stdout, stderr etc and streaming stdin.
730
+ """
731
+ main_cmd_args = self.git_main_cmd_args()
732
+ envs_vars = self.git_envs(popen_kwargs.pop("env", None))
733
+ cwd = popen_kwargs.pop("cwd", self.root_dir)
734
+ stdin = popen_kwargs.pop("stdin", PIPE)
735
+ stdout = popen_kwargs.pop("stdout", PIPE)
736
+ stderr = popen_kwargs.pop("stderr", PIPE)
737
+ bufsize = popen_kwargs.pop("bufsize", 0)
738
+ # Popen the git command
739
+ result = self.git.runner.popen_git_command(
740
+ main_cmd_args,
741
+ subcommand_args,
742
+ *popen_args,
743
+ text=text,
744
+ env=envs_vars,
745
+ cwd=cwd,
746
+ stdin=stdin,
747
+ stdout=stdout,
748
+ stderr=stderr,
749
+ bufsize=bufsize,
750
+ **popen_kwargs,
751
+ )
752
+ return result
753
+
754
+ def make_cmd(self, subcommand_args: list[str]) -> list[str]:
755
+ """
756
+ Make full runnable command for execution from the supplied ``subcommand_args``.
757
+
758
+ As knowledge of the git program and the main command is encapsulated within this ``UncheckedSubcmd`` thus,
759
+ this is a convenience method for any external entities that want to run commands in their own subprocess.
760
+
761
+ :param subcommand_args: arguments for subcommand.
762
+ :returns: a fully made and runnable command for some external ``subprocess`` call.
763
+ """
764
+ return self.git.runner.make_cmd(self.git.build_main_cmd_args(), subcommand_args)
765
+
766
+ def git_main_cmd_args(self) -> list[str]:
767
+ """
768
+ Get CLI args for git main cli command.
769
+
770
+ For example, ``--no-pager --no-advice`` is the git main command in ``git --no-pager --no-advice log master -1``.
771
+
772
+ :return: CLI args for git main cli command.
773
+ """
774
+ return self.git.build_main_cmd_args()
775
+
776
+ def git_envs(
777
+ self, extra_git_envs: dict[str, str] | None = None
778
+ ) -> dict[str, str] | None:
779
+ """
780
+ Get Git environment variables from the merged ``GitEnvVars`` object.
781
+
782
+ Skips values that are ``Unset`` or ``None``-like using ``not_none_not_unset()``.
783
+ Converts ``Path`` and ``datetime`` instances to ``str``.
784
+
785
+ :param extra_git_envs: extraneous git envs supplied by the caller. These will be merged into the resultant
786
+ git envs and then returned.
787
+ :return: A cleaned and normalized GitEnvVars dict suitable for use in subprocesses.
788
+ """
789
+ env_vars = self.git.build_git_envs()
790
+ if extra_git_envs and env_vars is not None:
791
+ env_vars.update(extra_git_envs)
792
+ return env_vars
@@ -9,7 +9,8 @@ from __future__ import annotations
9
9
 
10
10
  from abc import ABC
11
11
  from pathlib import Path
12
- from typing import override, Literal, overload
12
+ from subprocess import Popen
13
+ from typing import override, Literal, overload, Callable
13
14
 
14
15
  from vt.utils.commons.commons.op import RootDirOp
15
16
 
@@ -24,6 +25,7 @@ from gitbolt.subprocess import (
24
25
  UncheckedSubcmd,
25
26
  )
26
27
  from gitbolt.subprocess.add import AddCLIArgsBuilder
28
+ from gitbolt.subprocess.base import GitSession
27
29
  from gitbolt.subprocess.constants import VERSION_CMD
28
30
  from gitbolt.subprocess.ls_tree import LsTreeCLIArgsBuilder
29
31
  from gitbolt.subprocess.runner import GitCommandRunner
@@ -168,6 +170,7 @@ class UncheckedSubcmdImpl(UncheckedSubcmd, GitSubcmdCommandImpl):
168
170
 
169
171
 
170
172
  class SimpleGitCommand(GitCommand, RootDirOp):
173
+
171
174
  def __init__(
172
175
  self,
173
176
  git_root_dir: Path = Path.cwd(),
@@ -245,6 +248,15 @@ class SimpleGitCommand(GitCommand, RootDirOp):
245
248
  subcmd_unchecked._set_underlying_git(self)
246
249
  return subcmd_unchecked
247
250
 
251
+ def session(self, **commands: list[str] | Callable[[], Popen[bytes]]) -> GitSession:
252
+ cmds: dict[str, Callable[[], Popen[bytes]]] = {}
253
+ for cmd_name, runnable_cmd in commands.items():
254
+ if callable(runnable_cmd):
255
+ cmds[cmd_name] = runnable_cmd
256
+ else:
257
+ cmds[cmd_name] = lambda : self.subcmd_unchecked.popen(runnable_cmd, text=False)
258
+ return GitSession(self, **cmds)
259
+
248
260
 
249
261
  class CLISimpleGitCommand(SimpleGitCommand):
250
262
  """
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import pathlib
11
11
  from abc import abstractmethod
12
- from subprocess import CompletedProcess
12
+ from subprocess import CompletedProcess, Popen
13
13
  from typing import Protocol, overload, Any, Literal
14
14
 
15
15
 
@@ -64,6 +64,39 @@ class GitCommandRunner(Protocol):
64
64
  **subprocess_run_kwargs: Any,
65
65
  ) -> CompletedProcess[bytes]: ...
66
66
 
67
+ @overload
68
+ @abstractmethod
69
+ def popen_git_command(
70
+ self,
71
+ main_cmd_args: list[str],
72
+ subcommand_args: list[str],
73
+ *popen_run_args: Any,
74
+ text: Literal[False],
75
+ **popen_run_kwargs: Any,
76
+ ) -> Popen[bytes]: ...
77
+
78
+ @overload
79
+ @abstractmethod
80
+ def popen_git_command(
81
+ self,
82
+ main_cmd_args: list[str],
83
+ subcommand_args: list[str],
84
+ *popen_run_args: Any,
85
+ text: Literal[True],
86
+ **popen_run_kwargs: Any,
87
+ ) -> Popen[str]: ...
88
+
89
+ @abstractmethod
90
+ def make_cmd(self, main_cmd_args: list[str], sub_cmd_args: list[str]) -> list[str]:
91
+ """
92
+ Make command for execution for use in an external subprocess.
93
+
94
+ :param main_cmd_args: arguments for the main command.
95
+ :param sub_cmd_args: arguments for subcommand.
96
+ :returns: a fully made and runnable command for some external ``subprocess`` call.
97
+ """
98
+ ...
99
+
67
100
  @property
68
101
  @abstractmethod
69
102
  def git_prog(self) -> str | pathlib.Path:
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import pathlib
11
11
  import subprocess
12
- from subprocess import CompletedProcess
12
+ from subprocess import CompletedProcess, Popen
13
13
  from typing import overload, override, Any, Literal
14
14
 
15
15
  from gitbolt.subprocess.constants import GIT_CMD
@@ -86,7 +86,7 @@ class SimpleGitCR(GitCommandRunner):
86
86
  ) -> CompletedProcess[str] | CompletedProcess[bytes]:
87
87
  try:
88
88
  return subprocess.run(
89
- [str(self.git_prog), *main_cmd_args, *subcommand_args],
89
+ self.make_cmd(main_cmd_args, subcommand_args),
90
90
  *subprocess_run_args,
91
91
  input=_input,
92
92
  text=text,
@@ -97,6 +97,53 @@ class SimpleGitCR(GitCommandRunner):
97
97
  e.stderr, called_process_error=e, exit_code=e.returncode
98
98
  ) from e
99
99
 
100
+ @overload
101
+ @override
102
+ def popen_git_command(
103
+ self,
104
+ main_cmd_args: list[str],
105
+ subcommand_args: list[str],
106
+ *popen_run_args: Any,
107
+ text: Literal[False],
108
+ **popen_run_kwargs: Any,
109
+ ) -> Popen[bytes]: ...
110
+
111
+ @overload
112
+ @override
113
+ def popen_git_command(
114
+ self,
115
+ main_cmd_args: list[str],
116
+ subcommand_args: list[str],
117
+ *popen_run_args: Any,
118
+ text: Literal[True],
119
+ **popen_run_kwargs: Any,
120
+ ) -> Popen[str]: ...
121
+
122
+ @override
123
+ def popen_git_command(
124
+ self,
125
+ main_cmd_args: list[str],
126
+ subcommand_args: list[str],
127
+ *popen_run_args: Any,
128
+ text: Literal[True, False],
129
+ **popen_run_kwargs: Any,
130
+ ) -> Popen[str] | Popen[bytes]:
131
+ try:
132
+ return subprocess.Popen(
133
+ self.make_cmd(main_cmd_args, subcommand_args),
134
+ *popen_run_args,
135
+ text=text,
136
+ **popen_run_kwargs,
137
+ )
138
+ except subprocess.CalledProcessError as e:
139
+ raise GitCmdException(
140
+ e.stderr, called_process_error=e, exit_code=e.returncode
141
+ ) from e
142
+
143
+ @override
144
+ def make_cmd(self, main_cmd_args: list[str], sub_cmd_args: list[str]) -> list[str]:
145
+ return [str(self.git_prog), *main_cmd_args, *sub_cmd_args]
146
+
100
147
  @override
101
148
  @property
102
149
  def git_prog(self) -> str | pathlib.Path:
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python3
2
+ # coding=utf-8
3
+
4
+ """
5
+ Git command sessions utilities for long-running commands.
6
+
7
+ Create sessions for long-running commands and communicate with them using their stdin/stdout.
8
+
9
+ Much faster that subprocess creation for each input/output pair.
10
+ """
11
+ import subprocess
12
+
13
+ from vt.utils.commons.commons.core_py import read_exact
14
+
15
+
16
+ def cat_file_blob_content(cat_file_popen: subprocess.Popen[bytes], blob_hash: bytes) -> bytes:
17
+ """
18
+ Read git blob contents using a long-running batched cat-file process.
19
+
20
+ Spawning new ``git show`` processes can be slower and resource consuming.
21
+
22
+ :param cat_file_popen: long-running ``git cat-file --batch`` process in bytes mode.
23
+ :param blob_hash: hash to be read from cat-file.
24
+ :return: contents of blob hash in bytes.
25
+ """
26
+ write_obj = blob_hash + b"\n"
27
+ cat_file_popen.stdin.write(write_obj)
28
+ cat_file_popen.stdin.flush()
29
+ header = cat_file_popen.stdout.readline()
30
+ obj, typ, size = header.split()
31
+ blob_content: bytes = read_exact(cat_file_popen.stdout, int(size))
32
+ cat_file_popen.stdout.readline()
33
+ return blob_content
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitbolt
3
- Version: 0.0.0.dev15
3
+ Version: 0.0.0.dev17
4
4
  Summary: Fast, flexible and type-safe Git commands in Python.
5
5
  Author-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
6
6
  Maintainer-email: Suhas Krishna Srivastava <suhas.srivastava@vaastav.tech>
@@ -27,7 +27,7 @@ Requires-Python: >=3.12
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
29
  Requires-Dist: vt-err-hndlr>=0.0.0dev6
30
- Requires-Dist: vt-commons>=0.0.1.dev9
30
+ Requires-Dist: vt-commons>=0.0.1.dev11
31
31
  Provides-Extra: pygit2
32
32
  Requires-Dist: pygit2; extra == "pygit2"
33
33
  Provides-Extra: test
@@ -327,10 +327,12 @@ no_advice_reset_git = overridden_git.git_opts_override(no_advice=False)
327
327
 
328
328
  At last, run unchecked commands in git.
329
329
 
330
- Introduced in `0.0.0dev4` to
330
+ Introduced in `0.0.0.dev4` to
331
331
  - experiment.
332
332
  - have consistent interfaced commands run until all subcommands are provided by the library.
333
333
 
334
+ #### 🖥️ Run one process per command
335
+
334
336
  ```python
335
337
  import gitbolt
336
338
 
@@ -340,9 +342,33 @@ git.subcmd_unchecked.run(['--version']) # run the version option for git.
340
342
  git.subcmd_unchecked.run(['version']) # run the version subcommand.
341
343
  ```
342
344
 
345
+ #### 🖥️ Run one long-running process and communicate with it
346
+
347
+ Introduced in `0.0.0.dev16` to:
348
+ - Make communicable processes using `subprocess.Popen`.
349
+
350
+ Get a long-running process and communicate with it for batching and faster operations.
351
+
352
+ ```python
353
+ import gitbolt
354
+ import sys
355
+
356
+ git = gitbolt.get_git_command()
357
+ with git.subcmd_unchecked.popen(["cat-file", "--batch-command"]) as cf:
358
+ cf.stdin.write(b"contents HEAD\n")
359
+ cf.stdin.flush()
360
+ header = cf.stdout.readline().strip()
361
+ print(f"HEADER: {header}", file=sys.stderr)
362
+ obj, typ, size = header.split()
363
+ print(cf.stdout.read(int(size)))
364
+ cf.stdout.readline()
365
+ ```
366
+
367
+ Error handling and I/O management is left to the client/caller.
368
+
343
369
  #### 💻 Run commands received from CLI
344
370
 
345
- Introduced in `0.0.0dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
371
+ Introduced in `0.0.0.dev11` is the ability to take commands from CLI and run it inside `gitbolt`.
346
372
 
347
373
  While making a system it may be required to run cli commands as received from cli using gitbolt. An obvious example
348
374
  would be to make a system that receives CLI commands and does certain modifications/additions inside `gitbolt` before
@@ -25,6 +25,7 @@ src/gitbolt/subprocess/base.py
25
25
  src/gitbolt/subprocess/constants.py
26
26
  src/gitbolt/subprocess/exceptions.py
27
27
  src/gitbolt/subprocess/ls_tree.py
28
+ src/gitbolt/subprocess/session.py
28
29
  src/gitbolt/subprocess/utils.py
29
30
  src/gitbolt/subprocess/impl/__init__.py
30
31
  src/gitbolt/subprocess/impl/simple.py
@@ -1,5 +1,5 @@
1
1
  vt-err-hndlr>=0.0.0dev6
2
- vt-commons>=0.0.1.dev9
2
+ vt-commons>=0.0.1.dev11
3
3
 
4
4
  [pygit2]
5
5
  pygit2
File without changes
File without changes