abstract-pypit 0.0.1__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.
- abstract_pypit-0.0.1/PKG-INFO +80 -0
- abstract_pypit-0.0.1/README.md +57 -0
- abstract_pypit-0.0.1/pyproject.toml +37 -0
- abstract_pypit-0.0.1/setup.cfg +4 -0
- abstract_pypit-0.0.1/setup.py +21 -0
- abstract_pypit-0.0.1/src/abstract_pypit.egg-info/PKG-INFO +80 -0
- abstract_pypit-0.0.1/src/abstract_pypit.egg-info/SOURCES.txt +21 -0
- abstract_pypit-0.0.1/src/abstract_pypit.egg-info/dependency_links.txt +1 -0
- abstract_pypit-0.0.1/src/abstract_pypit.egg-info/entry_points.txt +3 -0
- abstract_pypit-0.0.1/src/abstract_pypit.egg-info/requires.txt +1 -0
- abstract_pypit-0.0.1/src/abstract_pypit.egg-info/top_level.txt +1 -0
- abstract_pypit-0.0.1/src/pypit/__init__.py +19 -0
- abstract_pypit-0.0.1/src/pypit/__main__.py +5 -0
- abstract_pypit-0.0.1/src/pypit/clean_the_repos.py +125 -0
- abstract_pypit-0.0.1/src/pypit/env_utils.py +120 -0
- abstract_pypit-0.0.1/src/pypit/github_auth.py +347 -0
- abstract_pypit-0.0.1/src/pypit/github_only.py +40 -0
- abstract_pypit-0.0.1/src/pypit/imports/__init__.py +8 -0
- abstract_pypit-0.0.1/src/pypit/imports/init_imports.py +30 -0
- abstract_pypit-0.0.1/src/pypit/imports/paths.py +103 -0
- abstract_pypit-0.0.1/src/pypit/imports/utils.py +95 -0
- abstract_pypit-0.0.1/src/pypit/main.py +160 -0
- abstract_pypit-0.0.1/src/pypit/pypit_utils.py +322 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abstract_pypit
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: One-command PyPI publisher + GitHub pusher. Version-bumps, builds, uploads to PyPI, commits and pushes to GitHub, and syncs the local install — all in one call.
|
|
5
|
+
Home-page: https://github.com/AbstractEndeavors/abstract_pypit
|
|
6
|
+
Author: putkoff
|
|
7
|
+
Author-email: putkoff <partners@abstractendeavors.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/AbstractEndeavors/abstract_pypit
|
|
10
|
+
Keywords: pypi,publish,release,github,automation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
|
|
24
|
+
# abstract_pypit
|
|
25
|
+
|
|
26
|
+
One-command PyPI publisher + GitHub pusher.
|
|
27
|
+
|
|
28
|
+
Finds the next free version above whatever is on PyPI, bumps `setup.py` and
|
|
29
|
+
`pyproject.toml`, builds sdist + wheel, uploads to PyPI via twine, commits and
|
|
30
|
+
pushes to GitHub, then syncs the local install — all in one call.
|
|
31
|
+
|
|
32
|
+
Zero dependencies outside the stdlib except `requests`.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install abstract_pypit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# from any package directory that has setup.py + pyproject.toml:
|
|
44
|
+
from pypit import runit
|
|
45
|
+
runit()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# or from the command line:
|
|
50
|
+
abstract-pypit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Credentials
|
|
54
|
+
|
|
55
|
+
**PyPI:** twine reads `~/.pypirc` or `TWINE_USERNAME` / `TWINE_PASSWORD` env vars.
|
|
56
|
+
|
|
57
|
+
**GitHub:** create `pypit/src/envs/.env` on the machine running pypit:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
GITHUB_OWNER_1=your-username
|
|
61
|
+
GITPASS_1=<your-github-pat>
|
|
62
|
+
GITHUB_OWNER_2=your-org
|
|
63
|
+
GITPASS_2=<org-github-pat>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
SSH key at `~/.ssh/github/githubssh_nopass` must be registered with GitHub.
|
|
67
|
+
|
|
68
|
+
## Per-package config
|
|
69
|
+
|
|
70
|
+
Add to the package's own `pyproject.toml`:
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
[tool.pypit]
|
|
74
|
+
github_owner = "your-org-or-username" # which org/user owns the repo
|
|
75
|
+
github_push = true # set false to skip GitHub entirely
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# abstract_pypit
|
|
2
|
+
|
|
3
|
+
One-command PyPI publisher + GitHub pusher.
|
|
4
|
+
|
|
5
|
+
Finds the next free version above whatever is on PyPI, bumps `setup.py` and
|
|
6
|
+
`pyproject.toml`, builds sdist + wheel, uploads to PyPI via twine, commits and
|
|
7
|
+
pushes to GitHub, then syncs the local install — all in one call.
|
|
8
|
+
|
|
9
|
+
Zero dependencies outside the stdlib except `requests`.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pip install abstract_pypit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# from any package directory that has setup.py + pyproject.toml:
|
|
21
|
+
from pypit import runit
|
|
22
|
+
runit()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
# or from the command line:
|
|
27
|
+
abstract-pypit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Credentials
|
|
31
|
+
|
|
32
|
+
**PyPI:** twine reads `~/.pypirc` or `TWINE_USERNAME` / `TWINE_PASSWORD` env vars.
|
|
33
|
+
|
|
34
|
+
**GitHub:** create `pypit/src/envs/.env` on the machine running pypit:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
GITHUB_OWNER_1=your-username
|
|
38
|
+
GITPASS_1=<your-github-pat>
|
|
39
|
+
GITHUB_OWNER_2=your-org
|
|
40
|
+
GITPASS_2=<org-github-pat>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
SSH key at `~/.ssh/github/githubssh_nopass` must be registered with GitHub.
|
|
44
|
+
|
|
45
|
+
## Per-package config
|
|
46
|
+
|
|
47
|
+
Add to the package's own `pyproject.toml`:
|
|
48
|
+
|
|
49
|
+
```toml
|
|
50
|
+
[tool.pypit]
|
|
51
|
+
github_owner = "your-org-or-username" # which org/user owns the repo
|
|
52
|
+
github_push = true # set false to skip GitHub entirely
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "abstract_pypit"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "One-command PyPI publisher + GitHub pusher. Version-bumps, builds, uploads to PyPI, commits and pushes to GitHub, and syncs the local install — all in one call."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "putkoff", email = "partners@abstractendeavors.com" }]
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
keywords = ["pypi", "publish", "release", "github", "automation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Topic :: Software Development :: Build Tools",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"requests",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/AbstractEndeavors/abstract_pypit"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
abstract-pypit = "pypit.main:runPypit"
|
|
31
|
+
pypit-github = "pypit.github_only:runGithubOnly"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-dir]
|
|
37
|
+
"" = "src"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="abstract_pypit",
|
|
5
|
+
version='0.0.1',
|
|
6
|
+
description="One-command PyPI publisher + GitHub pusher.",
|
|
7
|
+
author="putkoff",
|
|
8
|
+
author_email="partners@abstractendeavors.com",
|
|
9
|
+
license="MIT",
|
|
10
|
+
python_requires=">=3.8",
|
|
11
|
+
package_dir={"": "src"},
|
|
12
|
+
packages=find_packages(where="src"),
|
|
13
|
+
install_requires=["requests"],
|
|
14
|
+
entry_points={
|
|
15
|
+
"console_scripts": [
|
|
16
|
+
"abstract-pypit = pypit.main:runPypit",
|
|
17
|
+
"pypit-github = pypit.github_only:runGithubOnly",
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
url="https://github.com/AbstractEndeavors/abstract_pypit",
|
|
21
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abstract_pypit
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: One-command PyPI publisher + GitHub pusher. Version-bumps, builds, uploads to PyPI, commits and pushes to GitHub, and syncs the local install — all in one call.
|
|
5
|
+
Home-page: https://github.com/AbstractEndeavors/abstract_pypit
|
|
6
|
+
Author: putkoff
|
|
7
|
+
Author-email: putkoff <partners@abstractendeavors.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/AbstractEndeavors/abstract_pypit
|
|
10
|
+
Keywords: pypi,publish,release,github,automation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
|
|
24
|
+
# abstract_pypit
|
|
25
|
+
|
|
26
|
+
One-command PyPI publisher + GitHub pusher.
|
|
27
|
+
|
|
28
|
+
Finds the next free version above whatever is on PyPI, bumps `setup.py` and
|
|
29
|
+
`pyproject.toml`, builds sdist + wheel, uploads to PyPI via twine, commits and
|
|
30
|
+
pushes to GitHub, then syncs the local install — all in one call.
|
|
31
|
+
|
|
32
|
+
Zero dependencies outside the stdlib except `requests`.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install abstract_pypit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# from any package directory that has setup.py + pyproject.toml:
|
|
44
|
+
from pypit import runit
|
|
45
|
+
runit()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# or from the command line:
|
|
50
|
+
abstract-pypit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Credentials
|
|
54
|
+
|
|
55
|
+
**PyPI:** twine reads `~/.pypirc` or `TWINE_USERNAME` / `TWINE_PASSWORD` env vars.
|
|
56
|
+
|
|
57
|
+
**GitHub:** create `pypit/src/envs/.env` on the machine running pypit:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
GITHUB_OWNER_1=your-username
|
|
61
|
+
GITPASS_1=<your-github-pat>
|
|
62
|
+
GITHUB_OWNER_2=your-org
|
|
63
|
+
GITPASS_2=<org-github-pat>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
SSH key at `~/.ssh/github/githubssh_nopass` must be registered with GitHub.
|
|
67
|
+
|
|
68
|
+
## Per-package config
|
|
69
|
+
|
|
70
|
+
Add to the package's own `pyproject.toml`:
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
[tool.pypit]
|
|
74
|
+
github_owner = "your-org-or-username" # which org/user owns the repo
|
|
75
|
+
github_push = true # set false to skip GitHub entirely
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/abstract_pypit.egg-info/PKG-INFO
|
|
5
|
+
src/abstract_pypit.egg-info/SOURCES.txt
|
|
6
|
+
src/abstract_pypit.egg-info/dependency_links.txt
|
|
7
|
+
src/abstract_pypit.egg-info/entry_points.txt
|
|
8
|
+
src/abstract_pypit.egg-info/requires.txt
|
|
9
|
+
src/abstract_pypit.egg-info/top_level.txt
|
|
10
|
+
src/pypit/__init__.py
|
|
11
|
+
src/pypit/__main__.py
|
|
12
|
+
src/pypit/clean_the_repos.py
|
|
13
|
+
src/pypit/env_utils.py
|
|
14
|
+
src/pypit/github_auth.py
|
|
15
|
+
src/pypit/github_only.py
|
|
16
|
+
src/pypit/main.py
|
|
17
|
+
src/pypit/pypit_utils.py
|
|
18
|
+
src/pypit/imports/__init__.py
|
|
19
|
+
src/pypit/imports/init_imports.py
|
|
20
|
+
src/pypit/imports/paths.py
|
|
21
|
+
src/pypit/imports/utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pypit
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pypit (published as abstract_pypit)
|
|
3
|
+
=====================================
|
|
4
|
+
One-command PyPI publisher + GitHub pusher.
|
|
5
|
+
|
|
6
|
+
Usage as a library:
|
|
7
|
+
from pypit import runit, runGithubOnly
|
|
8
|
+
runit()
|
|
9
|
+
|
|
10
|
+
Usage from the command line (after pip install abstract_pypit):
|
|
11
|
+
python -m pypit
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.0.1"
|
|
15
|
+
|
|
16
|
+
from .main import runit, runPypit # noqa: F401
|
|
17
|
+
from .github_only import runGithubOnly # noqa: F401
|
|
18
|
+
|
|
19
|
+
__all__ = ["runit", "runPypit", "runGithubOnly", "__version__"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pypit.clean_the_repos
|
|
3
|
+
======================
|
|
4
|
+
Conflict-marker guard using git-tracked file list (not filesystem walk).
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from .imports import * # noqa: F401,F403
|
|
11
|
+
|
|
12
|
+
# ------------------------------------------------------------------------------
|
|
13
|
+
# A real git conflict block is anchored lines in order:
|
|
14
|
+
# <<<<<<< <label>
|
|
15
|
+
# [||||||| <label>] (diff3 style, optional)
|
|
16
|
+
# =======
|
|
17
|
+
# >>>>>>> <label>
|
|
18
|
+
# Anchoring at line start + exact 7 chars kills Cython comments and string
|
|
19
|
+
# literals that are not conflict markers.
|
|
20
|
+
# ------------------------------------------------------------------------------
|
|
21
|
+
_RE_OURS = re.compile(r"^<{7}(?: |$)")
|
|
22
|
+
_RE_BASE = re.compile(r"^\|{7}(?: |$)")
|
|
23
|
+
_RE_SPLIT = re.compile(r"^={7}$")
|
|
24
|
+
_RE_THEIRS = re.compile(r"^>{7}(?: |$)")
|
|
25
|
+
|
|
26
|
+
_SKIP_SUFFIXES = {
|
|
27
|
+
".whl", ".gz", ".zip", ".tar", ".png", ".jpg", ".jpeg", ".pdf", ".so",
|
|
28
|
+
".pyc", ".pyo", ".c", ".cpp", ".cc", ".h", ".hpp", ".pyx", ".pxd",
|
|
29
|
+
}
|
|
30
|
+
_MAX_BYTES = 5_000_000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _git_root(start="."):
|
|
34
|
+
out = subprocess.run(
|
|
35
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
36
|
+
cwd=os.path.abspath(start), capture_output=True, text=True,
|
|
37
|
+
)
|
|
38
|
+
if out.returncode != 0:
|
|
39
|
+
return None
|
|
40
|
+
return out.stdout.strip() or None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _repo_files(root):
|
|
44
|
+
found = []
|
|
45
|
+
for args in (
|
|
46
|
+
["git", "ls-files", "-z"],
|
|
47
|
+
["git", "ls-files", "--others", "--exclude-standard", "-z"],
|
|
48
|
+
):
|
|
49
|
+
out = subprocess.run(args, cwd=root, capture_output=True, text=True)
|
|
50
|
+
found += [f for f in out.stdout.split("\0") if f]
|
|
51
|
+
return list(dict.fromkeys(found))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _looks_text(path):
|
|
55
|
+
try:
|
|
56
|
+
if os.path.isdir(path):
|
|
57
|
+
return False
|
|
58
|
+
if os.path.splitext(path)[1].lower() in _SKIP_SUFFIXES:
|
|
59
|
+
return False
|
|
60
|
+
if os.path.getsize(path) > _MAX_BYTES:
|
|
61
|
+
return False
|
|
62
|
+
with open(path, "rb") as f:
|
|
63
|
+
return b"\x00" not in f.read(2048)
|
|
64
|
+
except OSError:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _scan_for_conflicts(path):
|
|
69
|
+
try:
|
|
70
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
71
|
+
text = f.read()
|
|
72
|
+
except OSError:
|
|
73
|
+
return []
|
|
74
|
+
if "<<<<<<<" not in text or ">>>>>>>" not in text:
|
|
75
|
+
return []
|
|
76
|
+
hits, block, state = [], [], "clean"
|
|
77
|
+
for ln, line in enumerate(text.splitlines(), 1):
|
|
78
|
+
if _RE_OURS.match(line):
|
|
79
|
+
block = [(ln, line.rstrip())]
|
|
80
|
+
state = "ours"
|
|
81
|
+
elif state == "ours" and _RE_BASE.match(line):
|
|
82
|
+
block.append((ln, line.rstrip()))
|
|
83
|
+
state = "base"
|
|
84
|
+
elif state in ("ours", "base") and _RE_SPLIT.match(line):
|
|
85
|
+
block.append((ln, line.rstrip()))
|
|
86
|
+
state = "split"
|
|
87
|
+
elif state == "split" and _RE_THEIRS.match(line):
|
|
88
|
+
block.append((ln, line.rstrip()))
|
|
89
|
+
hits.extend(block)
|
|
90
|
+
block, state = [], "clean"
|
|
91
|
+
return hits[:10]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def ensure_clean_repo(where="(unspecified)", *, require_clean_git=False, root="."):
|
|
95
|
+
git_root = _git_root(root)
|
|
96
|
+
if git_root is None:
|
|
97
|
+
print(f"ℹ️ ensure_clean_repo[{where}]: {os.path.abspath(root)} is not a git "
|
|
98
|
+
f"repo — skipping conflict scan")
|
|
99
|
+
return
|
|
100
|
+
offenders = {}
|
|
101
|
+
for rel in _repo_files(git_root):
|
|
102
|
+
p = os.path.join(git_root, rel)
|
|
103
|
+
if not _looks_text(p):
|
|
104
|
+
continue
|
|
105
|
+
hits = _scan_for_conflicts(p)
|
|
106
|
+
if hits:
|
|
107
|
+
offenders[p] = hits
|
|
108
|
+
if offenders:
|
|
109
|
+
lines = [f"\n🚫 Merge conflict markers detected {where}. Resolve before continuing:"]
|
|
110
|
+
for path, hits in offenders.items():
|
|
111
|
+
lines.append(f" - {path}")
|
|
112
|
+
for ln, t in hits:
|
|
113
|
+
lines.append(f" L{ln:>4}: {t}")
|
|
114
|
+
if len(hits) == 10:
|
|
115
|
+
lines.append(" ... (more lines truncated)")
|
|
116
|
+
raise RuntimeError("\n".join(lines))
|
|
117
|
+
if require_clean_git:
|
|
118
|
+
for args, err_msg in (
|
|
119
|
+
(["git", "diff", "--quiet"], "Unstaged changes in working tree."),
|
|
120
|
+
(["git", "diff", "--cached", "--quiet"], "Staged but uncommitted changes in index."),
|
|
121
|
+
):
|
|
122
|
+
rc = subprocess.call(args, cwd=git_root,
|
|
123
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
124
|
+
if rc != 0:
|
|
125
|
+
raise RuntimeError(err_msg)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pypit.env_utils
|
|
3
|
+
================
|
|
4
|
+
Standalone .env cascade reader — no third-party dependencies.
|
|
5
|
+
|
|
6
|
+
Inlined from abstract_security.envs; the dotenv/bcrypt/jwt surface of that
|
|
7
|
+
package is irrelevant here. Search order:
|
|
8
|
+
supplied path → cwd → home → ~/.envy_all → ~/envy_all
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
_DEFAULT_FILE = ".env"
|
|
14
|
+
_DEFAULT_KEY = "MY_PASSWORD"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _split_eq(line):
|
|
18
|
+
"""Split ``KEY=VALUE`` at the first '=' and strip whitespace."""
|
|
19
|
+
if "=" in line:
|
|
20
|
+
key, _, value = line.partition("=")
|
|
21
|
+
return key.strip(), value.strip()
|
|
22
|
+
return line.strip(), None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _search_file(key, path, deep_scan=False):
|
|
26
|
+
"""Return the value of *key* in the env file at *path*, or None."""
|
|
27
|
+
if not (path and os.path.isfile(path)):
|
|
28
|
+
return None
|
|
29
|
+
best_value, best_score = None, 0
|
|
30
|
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
31
|
+
for line in fh:
|
|
32
|
+
line_key, line_value = _split_eq(line)
|
|
33
|
+
if line_key == key:
|
|
34
|
+
return line_value
|
|
35
|
+
if deep_scan and line_key and key:
|
|
36
|
+
matched = sum(len(p) for p in key.split("_") if p and p in line_key)
|
|
37
|
+
if matched / len(key) >= 0.5 and matched > best_score:
|
|
38
|
+
best_value, best_score = line_value, matched
|
|
39
|
+
return best_value if deep_scan else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _candidate_dirs(start_path):
|
|
43
|
+
"""Return de-duplicated, existing directories to search."""
|
|
44
|
+
home = os.path.expanduser("~")
|
|
45
|
+
candidates = [
|
|
46
|
+
start_path,
|
|
47
|
+
os.getcwd(),
|
|
48
|
+
home,
|
|
49
|
+
os.path.join(home, ".envy_all"),
|
|
50
|
+
os.path.join(home, "envy_all"),
|
|
51
|
+
]
|
|
52
|
+
seen, result = set(), []
|
|
53
|
+
for d in candidates:
|
|
54
|
+
if d and d not in seen and os.path.isdir(d):
|
|
55
|
+
seen.add(d)
|
|
56
|
+
result.append(d)
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_env_value(key=_DEFAULT_KEY, path=None, file_name=_DEFAULT_FILE,
|
|
61
|
+
deep_scan=False):
|
|
62
|
+
"""
|
|
63
|
+
Read *key* from a .env-style file.
|
|
64
|
+
|
|
65
|
+
If *path* points directly to a file, that file is tried first.
|
|
66
|
+
Otherwise the cascade: supplied dir → cwd → home → ~/.envy_all.
|
|
67
|
+
"""
|
|
68
|
+
key = key or _DEFAULT_KEY
|
|
69
|
+
file_name = file_name or _DEFAULT_FILE
|
|
70
|
+
|
|
71
|
+
if path and os.path.isfile(path):
|
|
72
|
+
# direct file path given — search it first, then cascade from its dir
|
|
73
|
+
value = _search_file(key, path, deep_scan)
|
|
74
|
+
if value is not None:
|
|
75
|
+
return value
|
|
76
|
+
path = os.path.dirname(path)
|
|
77
|
+
|
|
78
|
+
for directory in _candidate_dirs(path or os.getcwd()):
|
|
79
|
+
value = _search_file(key, os.path.join(directory, file_name), deep_scan)
|
|
80
|
+
if value is not None:
|
|
81
|
+
return value
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_env_path(key=_DEFAULT_KEY, path=None, file_name=_DEFAULT_FILE,
|
|
86
|
+
deep_scan=False):
|
|
87
|
+
"""Return the path of the first .env file that contains *key*, or None."""
|
|
88
|
+
key = key or _DEFAULT_KEY
|
|
89
|
+
file_name = file_name or _DEFAULT_FILE
|
|
90
|
+
|
|
91
|
+
if path and os.path.isfile(path):
|
|
92
|
+
if _search_file(key, path, deep_scan) is not None:
|
|
93
|
+
return path
|
|
94
|
+
path = os.path.dirname(path)
|
|
95
|
+
|
|
96
|
+
for directory in _candidate_dirs(path or os.getcwd()):
|
|
97
|
+
env_path = os.path.join(directory, file_name)
|
|
98
|
+
if _search_file(key, env_path, deep_scan) is not None:
|
|
99
|
+
return env_path
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_initial_caller():
|
|
104
|
+
"""Return the path of the original entry-point script (sys.argv[0])."""
|
|
105
|
+
entry = sys.argv[0] if sys.argv else None
|
|
106
|
+
if entry:
|
|
107
|
+
return os.path.realpath(entry)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_initial_caller_dir():
|
|
112
|
+
"""Return the directory of the original entry-point script."""
|
|
113
|
+
caller = get_initial_caller()
|
|
114
|
+
return os.path.dirname(caller) if caller else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
__all__ = [
|
|
118
|
+
"get_env_value", "get_env_path",
|
|
119
|
+
"get_initial_caller", "get_initial_caller_dir",
|
|
120
|
+
]
|