ci-environment 10.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.
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.1
2
+ Name: ci-environment
3
+ Version: 10.0.1
4
+ Summary: This library detects the CI environment based on environment variables defined by CI servers.
5
+ Author-email: Cucumber Limited <cukes@googlegroups.com>
6
+ Maintainer-email: Konstantin Goloveshko <kostya.goloveshko@gmail.com>
7
+ License: MIT
8
+ Project-URL: Repository, https://github.com/cucumber/ci-environment
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: POSIX
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Topic :: Utilities
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: importlib_resources
24
+ Requires-Dist: python-dotenv
25
+ Provides-Extra: test
26
+ Requires-Dist: tox; extra == "test"
27
+
28
+ # CiEnvironment
29
+
30
+ Python implementation of https://github.com/cucumber/ci-environment
@@ -0,0 +1,3 @@
1
+ # CiEnvironment
2
+
3
+ Python implementation of https://github.com/cucumber/ci-environment
@@ -0,0 +1,69 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = ["setuptools", "wheel"]
4
+
5
+ [project]
6
+ authors = [
7
+ {name = "Cucumber Limited", email = "cukes@googlegroups.com"}
8
+ ]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: POSIX",
14
+ "Operating System :: Microsoft :: Windows",
15
+ "Operating System :: MacOS :: MacOS X",
16
+ "Topic :: Utilities",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.8",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11"
22
+ ]
23
+ dependencies = [
24
+ "importlib_resources",
25
+ "python-dotenv"
26
+ ]
27
+ dynamic = [
28
+ "optional-dependencies"
29
+ ]
30
+ description = "This library detects the CI environment based on environment variables defined by CI servers."
31
+ readme = {file = "README.md", content-type = "text/markdown"}
32
+ license = {text = "MIT"}
33
+ maintainers = [
34
+ {name = "Konstantin Goloveshko", email = "kostya.goloveshko@gmail.com"}
35
+ ]
36
+ name = "ci-environment"
37
+ requires-python = ">=3.8"
38
+ urls = {Repository = "https://github.com/cucumber/ci-environment"}
39
+ version = "10.0.1"
40
+
41
+ [project.entry-points]
42
+
43
+ [project.scripts]
44
+
45
+ [tool.black]
46
+ line-length = 120
47
+ target-version = ["py38", "py39", "py310", "py311"]
48
+ verbose = true
49
+
50
+ [tool.isort]
51
+ line_length = 120
52
+ multi_line_output = 3
53
+ profile = "black"
54
+
55
+ [tool.mypy]
56
+ files = "src/ci_environment/**/*.py"
57
+ install_types = true
58
+ non_interactive = true
59
+ plugins = [
60
+ "pydantic.mypy"
61
+ ]
62
+ show_error_codes = true
63
+ warn_return_any = true
64
+ warn_unused_configs = true
65
+
66
+ [[tool.mypy.overrides]]
67
+ ignore_missing_imports = true
68
+ module = [
69
+ ]
@@ -0,0 +1,21 @@
1
+ [options]
2
+ include_package_data = True
3
+ packages = find:
4
+ package_dir =
5
+ =src
6
+
7
+ [options.packages.find]
8
+ where = src
9
+
10
+ [options.package_data]
11
+ ci_environment =
12
+ CiEnvironments.json
13
+
14
+ [options.extras_require]
15
+ test =
16
+ tox
17
+
18
+ [egg_info]
19
+ tag_build =
20
+ tag_date = 0
21
+
@@ -0,0 +1,160 @@
1
+ [
2
+ {
3
+ "name": "Azure Pipelines",
4
+ "url": "${BUILD_BUILDURI}",
5
+ "buildNumber": "${BUILD_BUILDNUMBER}",
6
+ "git": {
7
+ "remote": "${BUILD_REPOSITORY_URI}",
8
+ "revision": "${BUILD_SOURCEVERSION}",
9
+ "branch": "${BUILD_SOURCEBRANCH/refs\/heads\/(.*)/\\1}",
10
+ "tag": "${BUILD_SOURCEBRANCH/refs\/tags\/(.*)/\\1}"
11
+ }
12
+ },
13
+ {
14
+ "name": "Bamboo",
15
+ "url": "${bamboo_buildResultsUrl}",
16
+ "buildNumber": "${bamboo_buildNumber}",
17
+ "git": {
18
+ "remote": "${bamboo_planRepository_repositoryUrl}",
19
+ "revision": "${bamboo_planRepository_revision}",
20
+ "branch": "${bamboo_planRepository_branch}"
21
+ }
22
+ },
23
+ {
24
+ "name": "Buddy",
25
+ "url": "${BUDDY_EXECUTION_URL}",
26
+ "buildNumber": "${BUDDY_EXECUTION_ID}",
27
+ "git": {
28
+ "remote": "${BUDDY_SCM_URL}",
29
+ "revision": "${BUDDY_EXECUTION_REVISION}",
30
+ "branch": "${BUDDY_EXECUTION_BRANCH}",
31
+ "tag": "${BUDDY_EXECUTION_TAG}"
32
+ }
33
+ },
34
+ {
35
+ "name": "Bitrise",
36
+ "url": "${BITRISE_BUILD_URL}",
37
+ "buildNumber": "${BITRISE_BUILD_NUMBER}",
38
+ "git": {
39
+ "remote": "${GIT_REPOSITORY_URL}",
40
+ "revision": "${BITRISE_GIT_COMMIT}",
41
+ "branch": "${BITRISE_GIT_BRANCH}",
42
+ "tag": "${BITRISE_GIT_TAG}"
43
+ }
44
+ },
45
+ {
46
+ "name": "CircleCI",
47
+ "url": "${CIRCLE_BUILD_URL}",
48
+ "buildNumber": "${CIRCLE_BUILD_NUM}",
49
+ "git": {
50
+ "remote": "${CIRCLE_REPOSITORY_URL}",
51
+ "revision": "${CIRCLE_SHA1}",
52
+ "branch": "${CIRCLE_BRANCH}",
53
+ "tag": "${CIRCLE_TAG}"
54
+ }
55
+ },
56
+ {
57
+ "name": "CodeFresh",
58
+ "url": "${CF_BUILD_URL}",
59
+ "buildNumber": "${CF_BUILD_ID}",
60
+ "git": {
61
+ "remote": "${CF_COMMIT_URL/(.*)\\/commit.+$/\\1}.git",
62
+ "revision": "${CF_REVISION}",
63
+ "branch": "${CF_BRANCH}"
64
+ }
65
+ },
66
+ {
67
+ "name": "CodeShip",
68
+ "url": "${CI_BUILD_URL}",
69
+ "buildNumber": "${CI_BUILD_NUMBER}",
70
+ "git": {
71
+ "remote": "${CI_PULL_REQUEST/(.*)\\/pull\\/\\d+/\\1.git}",
72
+ "revision": "${CI_COMMIT_ID}",
73
+ "branch": "${CI_BRANCH}"
74
+ }
75
+ },
76
+ {
77
+ "name": "GitHub Actions",
78
+ "url": "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}",
79
+ "buildNumber": "${GITHUB_RUN_ID}",
80
+ "git": {
81
+ "remote": "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git",
82
+ "revision": "${GITHUB_SHA}",
83
+ "branch": "${GITHUB_HEAD_REF}",
84
+ "tag": "${GITHUB_REF/refs\/tags\/(.*)/\\1}"
85
+ }
86
+ },
87
+ {
88
+ "name": "GitLab",
89
+ "url": "${CI_JOB_URL}",
90
+ "buildNumber": "${CI_JOB_ID}",
91
+ "git": {
92
+ "remote": "${CI_REPOSITORY_URL}",
93
+ "revision": "${CI_COMMIT_SHA}",
94
+ "branch": "${CI_COMMIT_BRANCH}",
95
+ "tag": "${CI_COMMIT_TAG}"
96
+ }
97
+ },
98
+ {
99
+ "name": "GoCD",
100
+ "url": "${GO_SERVER_URL}/pipelines/${GO_PIPELINE_NAME}/${GO_PIPELINE_COUNTER}/${GO_STAGE_NAME}/${GO_STAGE_COUNTER}",
101
+ "buildNumber": "${GO_PIPELINE_NAME}/${GO_PIPELINE_COUNTER}/${GO_STAGE_NAME}/${GO_STAGE_COUNTER}",
102
+ "git": {
103
+ "remote": "${GO_SCM_*_PR_URL/(.*)\\/pull\\/\\d+/\\1.git}",
104
+ "revision": "${GO_REVISION}",
105
+ "branch": "${GO_SCM_*_PR_BRANCH/.*:(.*)/\\1}"
106
+ }
107
+ },
108
+ {
109
+ "name": "Jenkins",
110
+ "url": "${BUILD_URL}",
111
+ "buildNumber": "${BUILD_NUMBER}",
112
+ "git": {
113
+ "remote": "${GIT_URL}",
114
+ "revision": "${GIT_COMMIT}",
115
+ "branch": "${GIT_LOCAL_BRANCH}"
116
+ }
117
+ },
118
+ {
119
+ "name": "JetBrains Space",
120
+ "url": "${JB_SPACE_EXECUTION_URL}",
121
+ "buildNumber": "${JB_SPACE_EXECUTION_NUMBER}",
122
+ "git": {
123
+ "remote": "https://${JB_SPACE_API_URL}/p/${JB_SPACE_PROJECT_KEY}/repositories/${JB_SPACE_GIT_REPOSITORY_NAME}",
124
+ "revision": "${JB_SPACE_GIT_REVISION}",
125
+ "branch": "${JB_SPACE_GIT_BRANCH}"
126
+ }
127
+ },
128
+ {
129
+ "name": "Semaphore",
130
+ "url": "${SEMAPHORE_ORGANIZATION_URL}/jobs/${SEMAPHORE_JOB_ID}",
131
+ "buildNumber": "${SEMAPHORE_JOB_ID}",
132
+ "git": {
133
+ "remote": "${SEMAPHORE_GIT_URL}",
134
+ "revision": "${SEMAPHORE_GIT_SHA}",
135
+ "branch": "${SEMAPHORE_GIT_BRANCH}",
136
+ "tag": "${SEMAPHORE_GIT_TAG_NAME}"
137
+ }
138
+ },
139
+ {
140
+ "name": "Travis CI",
141
+ "url": "${TRAVIS_BUILD_WEB_URL}",
142
+ "buildNumber": "${TRAVIS_JOB_NUMBER}",
143
+ "git": {
144
+ "remote": "https://github.com/${TRAVIS_REPO_SLUG}.git",
145
+ "revision": "${TRAVIS_COMMIT}",
146
+ "branch": "${TRAVIS_BRANCH}",
147
+ "tag": "${TRAVIS_TAG}"
148
+ }
149
+ },
150
+ {
151
+ "name": "Wercker",
152
+ "url": "${WERCKER_RUN_URL}",
153
+ "buildNumber": "${WERCKER_RUN_URL/.*\\/([^\\/]+)$/\\1}",
154
+ "git": {
155
+ "remote": "https://${WERCKER_GIT_DOMAIN}/${WERCKER_GIT_OWNER}/${WERCKER_GIT_REPOSITORY}.git",
156
+ "revision": "${WERCKER_GIT_COMMIT}",
157
+ "branch": "${WERCKER_GIT_BRANCH}"
158
+ }
159
+ }
160
+ ]
@@ -0,0 +1,3 @@
1
+ from ci_environment.ci_environment import detect_ci_environment
2
+
3
+ __all__ = ["detect_ci_environment"]
@@ -0,0 +1,89 @@
1
+ from collections import namedtuple
2
+ from contextlib import suppress
3
+ from functools import partial, wraps
4
+ from itertools import filterfalse
5
+ from json import loads as loads_json
6
+ from operator import is_
7
+ from pathlib import Path
8
+ from pprint import pformat
9
+ from typing import Dict
10
+ from urllib.parse import urlparse
11
+
12
+ from importlib_resources import as_file, files
13
+
14
+ from ci_environment.variable_expression import evaluate
15
+
16
+ _key_value = namedtuple("_key_value", ["key", "value"])
17
+
18
+
19
+ @wraps(dict)
20
+ def _opt_dict(*args, **kwargs) -> Dict:
21
+ return {k: v for k, v in dict(*args, **kwargs).items() if v is not None}
22
+
23
+
24
+ def _flip(func):
25
+ def wrapped(*args, **kwargs):
26
+ if len(args) > 1:
27
+ first, *other, last = args
28
+ return func(last, *other, first, **kwargs)
29
+ else:
30
+ return func(*args, **kwargs)
31
+
32
+ return wrapped
33
+
34
+
35
+ def detect_ci_environment(env: Dict[str, str]):
36
+ with as_file(files("ci_environment").joinpath("CiEnvironments.json")) as path:
37
+ ci_environments = loads_json(Path(path).read_text())
38
+
39
+ return next(filterfalse(partial(_flip(is_), None), map(partial(detect, env=env), ci_environments)), None)
40
+
41
+
42
+ def detect(ci_environment, env: Dict[str, str]):
43
+ if (url := evaluate(ci_environment["url"], env)) is not None:
44
+ return dict(
45
+ name=ci_environment["name"],
46
+ url=url,
47
+ buildNumber=evaluate(ci_environment["buildNumber"], env),
48
+ **_opt_dict(git=detect_git(ci_environment, env)),
49
+ )
50
+
51
+
52
+ def detect_git(ci_environment, env: Dict[str, str]):
53
+ if all(
54
+ [
55
+ (revision := detect_revision(ci_environment, env)) is not None,
56
+ (remote := evaluate(ci_environment["git"]["remote"], env)) is not None,
57
+ ]
58
+ ):
59
+ return dict(
60
+ remote=remove_userinfo_from_url(remote),
61
+ revision=revision,
62
+ **_opt_dict(
63
+ tag=evaluate(ci_environment.get("git", {}).get("tag"), env),
64
+ branch=evaluate(ci_environment.get("git", {}).get("branch"), env),
65
+ ),
66
+ )
67
+
68
+
69
+ def detect_revision(ci_environment, env: Dict[str, str]):
70
+ if env.get("GITHUB_EVENT_NAME") == "pull_request":
71
+ try:
72
+ event = loads_json(Path(env["GITHUB_EVENT_PATH"]).read_text())
73
+ except KeyError as e:
74
+ raise ValueError from e
75
+ try:
76
+ revision = event["pull_request"]["head"]["sha"]
77
+ except KeyError as e:
78
+ raise ValueError(pformat(event)) from e
79
+ else:
80
+ return revision
81
+ else:
82
+ return evaluate(ci_environment["git"]["revision"], env)
83
+
84
+
85
+ def remove_userinfo_from_url(remote):
86
+ if remote is not None:
87
+ with suppress(Exception):
88
+ return (parsed_url := urlparse(remote))._replace(netloc=parsed_url.hostname).geturl()
89
+ return remote
@@ -0,0 +1,43 @@
1
+ from contextlib import suppress
2
+ from re import compile as compile_re
3
+ from re import subn
4
+ from typing import Dict
5
+
6
+
7
+ def evaluate(expression, env):
8
+ if expression is None:
9
+ return
10
+ with suppress(Exception):
11
+ re_pattern = compile_re(r"\$\{(.*?)(?:(?<!\\)/(.*)/(.*))?}")
12
+
13
+ def repl(match):
14
+ variable = match.group(1)
15
+ value = get_value(env, variable)
16
+ if value is None:
17
+ raise ValueError
18
+ pattern = match.group(2)
19
+ if pattern is None:
20
+ return value
21
+ regexp = compile_re(subn("\\/", "/", pattern)[0])
22
+
23
+ replacement = match.group(3)
24
+ if regexp.match(value):
25
+ return regexp.subn(replacement, value)[0]
26
+ else:
27
+ raise ValueError
28
+
29
+ try:
30
+ result = re_pattern.subn(repl=repl, string=expression)[0]
31
+ return result
32
+ except ValueError:
33
+ ...
34
+
35
+
36
+ def get_value(env: Dict, variable: str):
37
+ if "*" in variable:
38
+ pattern = compile_re(variable.replace("*", ".*"))
39
+ for name, value in env.items():
40
+ if pattern.match(name):
41
+ return value
42
+ else:
43
+ return env.get(variable)
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.1
2
+ Name: ci-environment
3
+ Version: 10.0.1
4
+ Summary: This library detects the CI environment based on environment variables defined by CI servers.
5
+ Author-email: Cucumber Limited <cukes@googlegroups.com>
6
+ Maintainer-email: Konstantin Goloveshko <kostya.goloveshko@gmail.com>
7
+ License: MIT
8
+ Project-URL: Repository, https://github.com/cucumber/ci-environment
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: POSIX
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Topic :: Utilities
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: importlib_resources
24
+ Requires-Dist: python-dotenv
25
+ Provides-Extra: test
26
+ Requires-Dist: tox; extra == "test"
27
+
28
+ # CiEnvironment
29
+
30
+ Python implementation of https://github.com/cucumber/ci-environment
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.cfg
4
+ src/ci_environment/CiEnvironments.json
5
+ src/ci_environment/__init__.py
6
+ src/ci_environment/ci_environment.py
7
+ src/ci_environment/variable_expression.py
8
+ src/ci_environment.egg-info/PKG-INFO
9
+ src/ci_environment.egg-info/SOURCES.txt
10
+ src/ci_environment.egg-info/dependency_links.txt
11
+ src/ci_environment.egg-info/requires.txt
12
+ src/ci_environment.egg-info/top_level.txt
13
+ tests/test_detect_ci_environment.py
14
+ tests/test_variable_expression.py
@@ -0,0 +1,5 @@
1
+ importlib_resources
2
+ python-dotenv
3
+
4
+ [test]
5
+ tox
@@ -0,0 +1 @@
1
+ ci_environment
@@ -0,0 +1,21 @@
1
+ from json import loads
2
+ from pathlib import Path
3
+
4
+ from dotenv import dotenv_values
5
+ from pytest import mark
6
+
7
+ from ci_environment import detect_ci_environment
8
+
9
+ _data_path = Path(__file__) / ".." / ".." / ".." / "testdata"
10
+
11
+
12
+ @mark.parametrize("envfile", _data_path.glob("*.txt"))
13
+ def test_detect_ci_environment(envfile):
14
+ env = dotenv_values(envfile)
15
+ expected = loads((envfile.resolve().parent / f"{envfile.name}.json").read_text())
16
+
17
+ assert detect_ci_environment(env) == expected
18
+
19
+
20
+ def test_detect_ci_environment_when_no_environment():
21
+ assert detect_ci_environment({}) is None
@@ -0,0 +1,42 @@
1
+ from ci_environment.variable_expression import evaluate
2
+
3
+
4
+ def test_evaluate_returns_undefined_when_a_variable_iws_undefined():
5
+ expression = "hello-${SOME_VAR}"
6
+ result = evaluate(expression, {})
7
+ assert result is None
8
+
9
+
10
+ def test_evaluate_gets_a_value_without_replacement():
11
+ expression = "${SOME_VAR}"
12
+ result = evaluate(expression, {"SOME_VAR": "some_value"})
13
+ assert result == "some_value"
14
+
15
+
16
+ def test_evaluate_captures_a_group():
17
+ expression = "${SOME_REF/refs\\/heads\\/(.*)/\\1}"
18
+ result = evaluate(expression, {"SOME_REF": "refs/heads/main"})
19
+ assert result == "main"
20
+
21
+
22
+ def test_evaluate_works_with_star_wildcard_in_var():
23
+ expression = "${GO_SCM_*_PR_BRANCH/.*:(.*)/\\1}"
24
+ result = evaluate(
25
+ expression,
26
+ {
27
+ "GO_SCM_MY_MATERIAL_PR_BRANCH": "ashwankthkumar:feature-1",
28
+ },
29
+ )
30
+ assert result == "feature-1"
31
+
32
+
33
+ def test_evaluate_evaluates_a_complex_expression():
34
+ expression = "hello-${VAR1}-${VAR2/(.*) (.*)/\\2-\\1}-world"
35
+ result = evaluate(
36
+ expression,
37
+ {
38
+ "VAR1": "amazing",
39
+ "VAR2": "gorgeous beautiful",
40
+ },
41
+ )
42
+ assert result == "hello-amazing-beautiful-gorgeous-world"