pyrig 2.2.6__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 (102) hide show
  1. pyrig/__init__.py +1 -0
  2. pyrig/dev/__init__.py +6 -0
  3. pyrig/dev/builders/__init__.py +1 -0
  4. pyrig/dev/builders/base/__init__.py +5 -0
  5. pyrig/dev/builders/base/base.py +256 -0
  6. pyrig/dev/builders/pyinstaller.py +229 -0
  7. pyrig/dev/cli/__init__.py +5 -0
  8. pyrig/dev/cli/cli.py +95 -0
  9. pyrig/dev/cli/commands/__init__.py +1 -0
  10. pyrig/dev/cli/commands/build_artifacts.py +16 -0
  11. pyrig/dev/cli/commands/create_root.py +25 -0
  12. pyrig/dev/cli/commands/create_tests.py +244 -0
  13. pyrig/dev/cli/commands/init_project.py +160 -0
  14. pyrig/dev/cli/commands/make_inits.py +27 -0
  15. pyrig/dev/cli/commands/protect_repo.py +145 -0
  16. pyrig/dev/cli/shared_subcommands.py +20 -0
  17. pyrig/dev/cli/subcommands.py +73 -0
  18. pyrig/dev/configs/__init__.py +1 -0
  19. pyrig/dev/configs/base/__init__.py +5 -0
  20. pyrig/dev/configs/base/base.py +826 -0
  21. pyrig/dev/configs/containers/__init__.py +1 -0
  22. pyrig/dev/configs/containers/container_file.py +111 -0
  23. pyrig/dev/configs/dot_env.py +95 -0
  24. pyrig/dev/configs/dot_python_version.py +88 -0
  25. pyrig/dev/configs/git/__init__.py +5 -0
  26. pyrig/dev/configs/git/gitignore.py +181 -0
  27. pyrig/dev/configs/git/pre_commit.py +170 -0
  28. pyrig/dev/configs/licence.py +112 -0
  29. pyrig/dev/configs/markdown/__init__.py +1 -0
  30. pyrig/dev/configs/markdown/docs/__init__.py +1 -0
  31. pyrig/dev/configs/markdown/docs/index.py +38 -0
  32. pyrig/dev/configs/markdown/readme.py +132 -0
  33. pyrig/dev/configs/py_typed.py +28 -0
  34. pyrig/dev/configs/pyproject.py +436 -0
  35. pyrig/dev/configs/python/__init__.py +5 -0
  36. pyrig/dev/configs/python/builders_init.py +27 -0
  37. pyrig/dev/configs/python/configs_init.py +28 -0
  38. pyrig/dev/configs/python/dot_experiment.py +46 -0
  39. pyrig/dev/configs/python/main.py +59 -0
  40. pyrig/dev/configs/python/resources_init.py +27 -0
  41. pyrig/dev/configs/python/shared_subcommands.py +29 -0
  42. pyrig/dev/configs/python/src_init.py +27 -0
  43. pyrig/dev/configs/python/subcommands.py +27 -0
  44. pyrig/dev/configs/testing/__init__.py +5 -0
  45. pyrig/dev/configs/testing/conftest.py +64 -0
  46. pyrig/dev/configs/testing/fixtures_init.py +27 -0
  47. pyrig/dev/configs/testing/main_test.py +74 -0
  48. pyrig/dev/configs/testing/zero_test.py +43 -0
  49. pyrig/dev/configs/workflows/__init__.py +5 -0
  50. pyrig/dev/configs/workflows/base/__init__.py +5 -0
  51. pyrig/dev/configs/workflows/base/base.py +1662 -0
  52. pyrig/dev/configs/workflows/build.py +106 -0
  53. pyrig/dev/configs/workflows/health_check.py +133 -0
  54. pyrig/dev/configs/workflows/publish.py +68 -0
  55. pyrig/dev/configs/workflows/release.py +90 -0
  56. pyrig/dev/tests/__init__.py +5 -0
  57. pyrig/dev/tests/conftest.py +40 -0
  58. pyrig/dev/tests/fixtures/__init__.py +1 -0
  59. pyrig/dev/tests/fixtures/assertions.py +147 -0
  60. pyrig/dev/tests/fixtures/autouse/__init__.py +5 -0
  61. pyrig/dev/tests/fixtures/autouse/class_.py +42 -0
  62. pyrig/dev/tests/fixtures/autouse/module.py +40 -0
  63. pyrig/dev/tests/fixtures/autouse/session.py +589 -0
  64. pyrig/dev/tests/fixtures/factories.py +118 -0
  65. pyrig/dev/utils/__init__.py +1 -0
  66. pyrig/dev/utils/cli.py +17 -0
  67. pyrig/dev/utils/git.py +312 -0
  68. pyrig/dev/utils/packages.py +93 -0
  69. pyrig/dev/utils/resources.py +77 -0
  70. pyrig/dev/utils/testing.py +66 -0
  71. pyrig/dev/utils/versions.py +268 -0
  72. pyrig/main.py +9 -0
  73. pyrig/py.typed +0 -0
  74. pyrig/resources/GITIGNORE +216 -0
  75. pyrig/resources/LATEST_PYTHON_VERSION +1 -0
  76. pyrig/resources/MIT_LICENSE_TEMPLATE +21 -0
  77. pyrig/resources/__init__.py +1 -0
  78. pyrig/src/__init__.py +1 -0
  79. pyrig/src/git/__init__.py +6 -0
  80. pyrig/src/git/git.py +146 -0
  81. pyrig/src/graph.py +255 -0
  82. pyrig/src/iterate.py +107 -0
  83. pyrig/src/modules/__init__.py +22 -0
  84. pyrig/src/modules/class_.py +369 -0
  85. pyrig/src/modules/function.py +189 -0
  86. pyrig/src/modules/inspection.py +148 -0
  87. pyrig/src/modules/module.py +658 -0
  88. pyrig/src/modules/package.py +452 -0
  89. pyrig/src/os/__init__.py +6 -0
  90. pyrig/src/os/os.py +121 -0
  91. pyrig/src/project/__init__.py +5 -0
  92. pyrig/src/project/mgt.py +83 -0
  93. pyrig/src/resource.py +58 -0
  94. pyrig/src/string.py +100 -0
  95. pyrig/src/testing/__init__.py +6 -0
  96. pyrig/src/testing/assertions.py +66 -0
  97. pyrig/src/testing/convention.py +203 -0
  98. pyrig-2.2.6.dist-info/METADATA +174 -0
  99. pyrig-2.2.6.dist-info/RECORD +102 -0
  100. pyrig-2.2.6.dist-info/WHEEL +4 -0
  101. pyrig-2.2.6.dist-info/entry_points.txt +3 -0
  102. pyrig-2.2.6.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,268 @@
1
+ """Version parsing and constraint utilities.
2
+
3
+ This module provides utilities for working with Python version specifiers
4
+ and constraints. It wraps the `packaging` library to provide convenient
5
+ methods for extracting version bounds and generating version ranges.
6
+
7
+ The main class `VersionConstraint` parses PEP 440 version specifiers
8
+ (e.g., ">=3.8,<3.12") and provides methods to:
9
+ - Get inclusive/exclusive lower and upper bounds
10
+ - Generate lists of versions within a constraint
11
+ - Adjust version precision (major/minor/micro)
12
+
13
+ Example:
14
+ >>> from pyrig.src.project.versions import VersionConstraint
15
+ >>> vc = VersionConstraint(">=3.8,<3.12")
16
+ >>> vc.get_lower_inclusive()
17
+ <Version('3.8')>
18
+ >>> vc.get_version_range(level="minor")
19
+ [<Version('3.8')>, <Version('3.9')>, <Version('3.10')>, <Version('3.11')>]
20
+ """
21
+
22
+ from typing import Literal
23
+
24
+ from packaging.specifiers import SpecifierSet
25
+ from packaging.version import Version
26
+
27
+
28
+ def adjust_version_to_level(
29
+ version: Version, level: Literal["major", "minor", "micro"]
30
+ ) -> Version:
31
+ """Truncate a version to the specified precision level.
32
+
33
+ Args:
34
+ version: The version to adjust.
35
+ level: The precision level to truncate to.
36
+
37
+ Returns:
38
+ A new Version with components beyond the level removed.
39
+
40
+ Example:
41
+ >>> adjust_version_to_level(Version("3.11.5"), "minor")
42
+ <Version('3.11')>
43
+ """
44
+ if level == "major":
45
+ return Version(f"{version.major}")
46
+ if level == "minor":
47
+ return Version(f"{version.major}.{version.minor}")
48
+ return version
49
+
50
+
51
+ class VersionConstraint:
52
+ """Parser and analyzer for PEP 440 version constraints.
53
+
54
+ Parses version specifier strings (e.g., ">=3.8,<3.12") and provides
55
+ methods to extract bounds and generate version ranges. Handles both
56
+ inclusive and exclusive bounds, converting between them as needed.
57
+
58
+ Attributes:
59
+ constraint: The original constraint string.
60
+ spec: The cleaned specifier string (quotes stripped).
61
+ sset: The parsed SpecifierSet from the packaging library.
62
+ lower_inclusive: The effective lower bound (inclusive).
63
+ upper_exclusive: The effective upper bound (exclusive).
64
+
65
+ Example:
66
+ >>> vc = VersionConstraint(">=3.8,<3.12")
67
+ >>> vc.get_lower_inclusive()
68
+ <Version('3.8')>
69
+ >>> vc.get_upper_exclusive()
70
+ <Version('3.12')>
71
+ """
72
+
73
+ def __init__(self, constraint: str) -> None:
74
+ """Initialize a VersionConstraint from a specifier string.
75
+
76
+ Args:
77
+ constraint: A PEP 440 version specifier string (e.g., ">=3.8,<3.12").
78
+ """
79
+ self.constraint = constraint
80
+ self.spec = self.constraint.strip().strip('"').strip("'")
81
+ self.sset = SpecifierSet(self.spec)
82
+
83
+ self.lowers_inclusive = [
84
+ Version(s.version) for s in self.sset if s.operator == ">="
85
+ ]
86
+ self.lowers_exclusive = [
87
+ Version(s.version) for s in self.sset if s.operator == ">"
88
+ ]
89
+ # increment the last number of exclusive, so
90
+ # >3.4.1 to >=3.4.2; <3.4.0 to <=3.4.1; 3.0.0 to <=3.0.1
91
+ self.lowers_exclusive_to_inclusive = [
92
+ Version(f"{v.major}.{v.minor}.{v.micro + 1}") for v in self.lowers_exclusive
93
+ ]
94
+ self.lowers_inclusive = (
95
+ self.lowers_inclusive + self.lowers_exclusive_to_inclusive
96
+ )
97
+
98
+ self.uppers_inclusive = [
99
+ Version(s.version) for s in self.sset if s.operator == "<="
100
+ ]
101
+ self.uppers_exclusive = [
102
+ Version(s.version) for s in self.sset if s.operator == "<"
103
+ ]
104
+
105
+ # increment the last number of inclusive, so
106
+ # <=3.4.1 to <3.4.2; >=3.4.0 to >3.4.1; 3.0.0 to >3.0.1
107
+ self.uppers_inclusive_to_exclusive = [
108
+ Version(f"{v.major}.{v.minor}.{v.micro + 1}") for v in self.uppers_inclusive
109
+ ]
110
+ self.uppers_exclusive = (
111
+ self.uppers_inclusive_to_exclusive + self.uppers_exclusive
112
+ )
113
+
114
+ self.upper_exclusive = (
115
+ min(self.uppers_exclusive) if self.uppers_exclusive else None
116
+ )
117
+ self.lower_inclusive = (
118
+ max(self.lowers_inclusive) if self.lowers_inclusive else None
119
+ )
120
+
121
+ def get_lower_inclusive(
122
+ self, default: str | Version | None = None
123
+ ) -> Version | None:
124
+ """Get the minimum version.
125
+
126
+ Is given inclusive. E.g. >=3.8, <3.12 -> 3.8
127
+ if >3.7, <3.12 -> 3.7.1
128
+
129
+ E.g. >=3.8, <3.12 -> 3.8
130
+
131
+ Args:
132
+ default: The default value to return if there is no minimum version
133
+
134
+ Returns:
135
+ The minimum version
136
+ """
137
+ default = str(default) if default else None
138
+ if self.lower_inclusive is None:
139
+ return Version(default) if default else None
140
+
141
+ return self.lower_inclusive
142
+
143
+ def get_upper_exclusive(
144
+ self, default: str | Version | None = None
145
+ ) -> Version | None:
146
+ """Get the maximum version.
147
+
148
+ Is given exclusive. E.g. >=3.8, <3.12 -> 3.12
149
+ if >=3.8, <=3.12 -> 3.12.1
150
+
151
+ Args:
152
+ default: The default value to return if there is no maximum version
153
+
154
+ Returns:
155
+ The maximum version
156
+ """
157
+ default = str(default) if default else None
158
+ if self.upper_exclusive is None:
159
+ return Version(default) if default else None
160
+
161
+ return self.upper_exclusive
162
+
163
+ def get_upper_inclusive(
164
+ self, default: str | Version | None = None
165
+ ) -> Version | None:
166
+ """Get the maximum version.
167
+
168
+ Is given inclusive. E.g. >=3.8, <3.12 -> 3.11
169
+ if >=3.8, <=3.12 -> 3.12
170
+
171
+ Args:
172
+ default: The default value to return if there is no maximum version
173
+
174
+ Returns:
175
+ The maximum version
176
+ """
177
+ # increment the default by 1 micro to make it exclusive
178
+ if default:
179
+ default = Version(str(default))
180
+ default = Version(f"{default.major}.{default.minor}.{default.micro + 1}")
181
+ upper_exclusive = self.get_upper_exclusive(default)
182
+ if upper_exclusive is None:
183
+ return None
184
+
185
+ if upper_exclusive.micro != 0:
186
+ return Version(
187
+ f"{upper_exclusive.major}.{upper_exclusive.minor}.{upper_exclusive.micro - 1}" # noqa: E501
188
+ )
189
+ if upper_exclusive.minor != 0:
190
+ return Version(f"{upper_exclusive.major}.{upper_exclusive.minor - 1}")
191
+ return Version(f"{upper_exclusive.major - 1}")
192
+
193
+ def get_version_range(
194
+ self,
195
+ level: Literal["major", "minor", "micro"] = "major",
196
+ lower_default: str | Version | None = None,
197
+ upper_default: str | Version | None = None,
198
+ ) -> list[Version]:
199
+ """Get the version range.
200
+
201
+ returns a range of versions according to the level
202
+
203
+ E.g. >=3.8, <3.12; level=major -> 3
204
+ >=3.8, <4.12; level=major -> 3, 4
205
+ E.g. >=3.8, <=3.12; level=minor -> 3.8, 3.9, 3.10, 3.11, 3.12
206
+ E.g. >=3.8.1, <=4.12.1; level=micro -> 3.8.1, 3.8.2, ... 4.12.1
207
+
208
+ Args:
209
+ level: The level of the version to return
210
+ lower_default: The default lower bound if none is specified
211
+ upper_default: The default upper bound if none is specified
212
+
213
+ Returns:
214
+ A list of versions
215
+ """
216
+ lower = self.get_lower_inclusive(lower_default)
217
+ upper = self.get_upper_inclusive(upper_default)
218
+
219
+ if lower is None or upper is None:
220
+ msg = "No lower or upper bound. Please specify default values."
221
+ raise ValueError(msg)
222
+
223
+ major_level, minor_level, micro_level = range(3)
224
+ level_int = {"major": major_level, "minor": minor_level, "micro": micro_level}[
225
+ level
226
+ ]
227
+ lower_as_list = [lower.major, lower.minor, lower.micro]
228
+ upper_as_list = [upper.major, upper.minor, upper.micro]
229
+
230
+ versions: list[list[int]] = []
231
+ for major in range(lower_as_list[major_level], upper_as_list[major_level] + 1):
232
+ version = [major]
233
+
234
+ minor_lower_og, minor_upper_og = (
235
+ lower_as_list[minor_level],
236
+ upper_as_list[minor_level],
237
+ )
238
+ diff = minor_upper_og - minor_lower_og
239
+ minor_lower = minor_lower_og if diff >= 0 else 0
240
+ minor_upper = minor_upper_og if diff >= 0 else minor_lower_og + abs(diff)
241
+ for minor in range(
242
+ minor_lower,
243
+ minor_upper + 1,
244
+ ):
245
+ # pop the minor if one already exists
246
+ if len(version) > minor_level:
247
+ version.pop()
248
+
249
+ version.append(minor)
250
+
251
+ micro_lower_og, micro_upper_og = (
252
+ lower_as_list[micro_level],
253
+ upper_as_list[micro_level],
254
+ )
255
+ diff = micro_upper_og - micro_lower_og
256
+ micro_lower = micro_lower_og if diff >= 0 else 0
257
+ micro_upper = (
258
+ micro_upper_og if diff >= 0 else micro_lower_og + abs(diff)
259
+ )
260
+ for micro in range(
261
+ micro_lower,
262
+ micro_upper + 1,
263
+ ):
264
+ version.append(micro)
265
+ versions.append(version[: level_int + 1])
266
+ version.pop()
267
+ version_versions = sorted({Version(".".join(map(str, v))) for v in versions})
268
+ return [v for v in version_versions if self.sset.contains(v)]
pyrig/main.py ADDED
@@ -0,0 +1,9 @@
1
+ """Main entrypoint for the project."""
2
+
3
+
4
+ def main() -> None:
5
+ """Main entrypoint for the project."""
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
pyrig/py.typed ADDED
File without changes
@@ -0,0 +1,216 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+
204
+ # Ruff stuff:
205
+ .ruff_cache/
206
+
207
+ # PyPI configuration file
208
+ .pypirc
209
+
210
+ # Marimo
211
+ marimo/_static/
212
+ marimo/_lsp/
213
+ __marimo__/
214
+
215
+ # Streamlit
216
+ .streamlit/secrets.toml
@@ -0,0 +1 @@
1
+ 3.14.2
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [year] [fullname]
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 @@
1
+ """__init__ module."""
pyrig/src/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """src package."""
@@ -0,0 +1,6 @@
1
+ """Git and GitHub integration utilities.
2
+
3
+ This package provides utilities for interacting with Git repositories and
4
+ the GitHub API, including repository information extraction and GitHub
5
+ Actions detection.
6
+ """
pyrig/src/git/git.py ADDED
@@ -0,0 +1,146 @@
1
+ """GitHub repository utilities for token management and URL parsing.
2
+
3
+ This module provides utilities for working with GitHub repositories,
4
+ including authentication token retrieval, GitHub Actions environment
5
+ detection, and repository URL parsing.
6
+
7
+ The token retrieval supports both environment variables and .env files,
8
+ following a priority order that prefers environment variables for CI/CD
9
+ compatibility.
10
+
11
+ Example:
12
+ >>> from pyrig.src.git.github.github import get_repo_owner_and_name_from_git
13
+ >>> owner, repo = get_repo_owner_and_name_from_git()
14
+ >>> print(f"{owner}/{repo}")
15
+ myorg/myrepo
16
+ """
17
+
18
+ import os
19
+ from pathlib import Path
20
+ from subprocess import CompletedProcess # nosec: B404
21
+
22
+ from pyrig.src.modules.package import get_project_name_from_cwd
23
+ from pyrig.src.os.os import run_subprocess
24
+
25
+
26
+ def running_in_github_actions() -> bool:
27
+ """Check if the code is running inside a GitHub Actions workflow.
28
+
29
+ GitHub Actions sets the `GITHUB_ACTIONS` environment variable to "true"
30
+ in all workflow runs. This function checks for that variable.
31
+
32
+ Returns:
33
+ True if running in GitHub Actions, False otherwise.
34
+
35
+ Example:
36
+ >>> if running_in_github_actions():
37
+ ... print("Running in CI")
38
+ ... else:
39
+ ... print("Running locally")
40
+ """
41
+ return os.getenv("GITHUB_ACTIONS", "false") == "true"
42
+
43
+
44
+ def get_repo_url_from_git(*, check: bool = True) -> str:
45
+ """Get the remote origin URL from the local git repository.
46
+
47
+ Executes `git config --get remote.origin.url` to retrieve the URL
48
+ of the origin remote.
49
+ Url can be:
50
+ - https://github.com/owner/repo.git
51
+ - git@github.com:owner/repo.git
52
+
53
+ Args:
54
+ check: Whether to check succes in subprocess.
55
+
56
+ Returns:
57
+ The remote origin URL (e.g., "https://github.com/owner/repo.git"
58
+ or "git@github.com:owner/repo.git").
59
+
60
+ Raises:
61
+ subprocess.CalledProcessError: If not in a git repository or if
62
+ the origin remote is not configured.
63
+ """
64
+ stdout: str = run_subprocess(
65
+ ["git", "config", "--get", "remote.origin.url"], check=check
66
+ ).stdout.decode("utf-8")
67
+ return stdout.strip()
68
+
69
+
70
+ def get_git_username() -> str:
71
+ """Get the git username from the local git config.
72
+
73
+ Executes `git config --get user.name` to retrieve the username.
74
+
75
+ Returns:
76
+ The git username.
77
+
78
+ Raises:
79
+ subprocess.CalledProcessError: If the username cannot be read.
80
+ """
81
+ stdout: str = run_subprocess(["git", "config", "--get", "user.name"]).stdout.decode(
82
+ "utf-8"
83
+ )
84
+ return stdout.strip()
85
+
86
+
87
+ def get_repo_owner_and_name_from_git(*, check_repo_url: bool = True) -> tuple[str, str]:
88
+ """Extract the GitHub owner and repository name from the git remote.
89
+
90
+ Parses the remote origin URL to extract the owner (organization or user)
91
+ and repository name. Handles both HTTPS and SSH URL formats.
92
+
93
+ Args:
94
+ check_repo_url: Whether to check succes in subprocess.
95
+
96
+ Returns:
97
+ A tuple of (owner, repository_name).
98
+
99
+ Raises:
100
+ subprocess.CalledProcessError: If the git remote cannot be read.
101
+
102
+ Example:
103
+ >>> owner, repo = get_repo_owner_and_name_from_git()
104
+ >>> print(f"{owner}/{repo}")
105
+ myorg/myrepo
106
+ """
107
+ url = get_repo_url_from_git(check=check_repo_url)
108
+ if not url:
109
+ # we default to git username and repo name from cwd
110
+ owner = get_git_username()
111
+ repo = get_project_name_from_cwd()
112
+ return owner, repo
113
+
114
+ parts = url.removesuffix(".git").split("/")
115
+ # keep last two parts
116
+ owner, repo = parts[-2:]
117
+ if ":" in owner:
118
+ owner = owner.split(":")[-1]
119
+ return owner, repo
120
+
121
+
122
+ def get_git_unstaged_changes() -> str:
123
+ """Check if the git repository has uncommitted changes.
124
+
125
+ Returns:
126
+ The output of git diff
127
+ """
128
+ completed_process = run_subprocess(["git", "diff"])
129
+ unstaged_changes: str = completed_process.stdout.decode("utf-8")
130
+ return unstaged_changes
131
+
132
+
133
+ def git_add_file(path: Path, *, check: bool = True) -> CompletedProcess[bytes]:
134
+ """Add a file to the git index.
135
+
136
+ Args:
137
+ path: Path to the file to add.
138
+ check: Whether to check succes in subprocess.
139
+
140
+ Returns:
141
+ The completed process result.
142
+ """
143
+ # make path relative to cwd if it is absolute
144
+ if path.is_absolute():
145
+ path = path.relative_to(Path.cwd())
146
+ return run_subprocess(["git", "add", str(path)], check=check)