codecov-cli 11.0.0__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 (83) hide show
  1. codecov_cli/__init__.py +3 -0
  2. codecov_cli/commands/__init__.py +0 -0
  3. codecov_cli/commands/base_picking.py +75 -0
  4. codecov_cli/commands/commit.py +72 -0
  5. codecov_cli/commands/create_report_result.py +41 -0
  6. codecov_cli/commands/empty_upload.py +80 -0
  7. codecov_cli/commands/get_report_results.py +50 -0
  8. codecov_cli/commands/labelanalysis.py +269 -0
  9. codecov_cli/commands/process_test_results.py +273 -0
  10. codecov_cli/commands/report.py +65 -0
  11. codecov_cli/commands/send_notifications.py +46 -0
  12. codecov_cli/commands/staticanalysis.py +62 -0
  13. codecov_cli/commands/upload.py +316 -0
  14. codecov_cli/commands/upload_coverage.py +186 -0
  15. codecov_cli/commands/upload_process.py +133 -0
  16. codecov_cli/fallbacks.py +41 -0
  17. codecov_cli/helpers/__init__.py +0 -0
  18. codecov_cli/helpers/args.py +31 -0
  19. codecov_cli/helpers/ci_adapters/__init__.py +63 -0
  20. codecov_cli/helpers/ci_adapters/appveyor_ci.py +54 -0
  21. codecov_cli/helpers/ci_adapters/azure_pipelines.py +44 -0
  22. codecov_cli/helpers/ci_adapters/base.py +102 -0
  23. codecov_cli/helpers/ci_adapters/bitbucket_ci.py +42 -0
  24. codecov_cli/helpers/ci_adapters/bitrise_ci.py +37 -0
  25. codecov_cli/helpers/ci_adapters/buildkite.py +45 -0
  26. codecov_cli/helpers/ci_adapters/circleci.py +47 -0
  27. codecov_cli/helpers/ci_adapters/cirrus_ci.py +36 -0
  28. codecov_cli/helpers/ci_adapters/cloudbuild.py +70 -0
  29. codecov_cli/helpers/ci_adapters/codebuild.py +49 -0
  30. codecov_cli/helpers/ci_adapters/droneci.py +36 -0
  31. codecov_cli/helpers/ci_adapters/github_actions.py +90 -0
  32. codecov_cli/helpers/ci_adapters/gitlab_ci.py +56 -0
  33. codecov_cli/helpers/ci_adapters/heroku.py +36 -0
  34. codecov_cli/helpers/ci_adapters/jenkins.py +38 -0
  35. codecov_cli/helpers/ci_adapters/local.py +39 -0
  36. codecov_cli/helpers/ci_adapters/teamcity.py +37 -0
  37. codecov_cli/helpers/ci_adapters/travis_ci.py +44 -0
  38. codecov_cli/helpers/ci_adapters/woodpeckerci.py +36 -0
  39. codecov_cli/helpers/config.py +66 -0
  40. codecov_cli/helpers/encoder.py +49 -0
  41. codecov_cli/helpers/folder_searcher.py +114 -0
  42. codecov_cli/helpers/git.py +97 -0
  43. codecov_cli/helpers/git_services/__init__.py +14 -0
  44. codecov_cli/helpers/git_services/github.py +40 -0
  45. codecov_cli/helpers/glob.py +146 -0
  46. codecov_cli/helpers/logging_utils.py +77 -0
  47. codecov_cli/helpers/options.py +51 -0
  48. codecov_cli/helpers/request.py +198 -0
  49. codecov_cli/helpers/upload_type.py +15 -0
  50. codecov_cli/helpers/validators.py +13 -0
  51. codecov_cli/helpers/versioning_systems.py +201 -0
  52. codecov_cli/main.py +99 -0
  53. codecov_cli/opentelemetry.py +26 -0
  54. codecov_cli/plugins/__init__.py +92 -0
  55. codecov_cli/plugins/compress_pycoverage_contexts.py +141 -0
  56. codecov_cli/plugins/gcov.py +69 -0
  57. codecov_cli/plugins/pycoverage.py +134 -0
  58. codecov_cli/plugins/types.py +8 -0
  59. codecov_cli/plugins/xcode.py +117 -0
  60. codecov_cli/runners/__init__.py +80 -0
  61. codecov_cli/runners/dan_runner.py +64 -0
  62. codecov_cli/runners/pytest_standard_runner.py +184 -0
  63. codecov_cli/runners/types.py +33 -0
  64. codecov_cli/services/__init__.py +0 -0
  65. codecov_cli/services/commit/__init__.py +86 -0
  66. codecov_cli/services/commit/base_picking.py +24 -0
  67. codecov_cli/services/empty_upload/__init__.py +42 -0
  68. codecov_cli/services/report/__init__.py +169 -0
  69. codecov_cli/services/upload/__init__.py +169 -0
  70. codecov_cli/services/upload/file_finder.py +320 -0
  71. codecov_cli/services/upload/legacy_upload_sender.py +132 -0
  72. codecov_cli/services/upload/network_finder.py +49 -0
  73. codecov_cli/services/upload/upload_collector.py +198 -0
  74. codecov_cli/services/upload/upload_sender.py +232 -0
  75. codecov_cli/services/upload_completion/__init__.py +38 -0
  76. codecov_cli/services/upload_coverage/__init__.py +93 -0
  77. codecov_cli/types.py +88 -0
  78. codecov_cli-11.0.0.dist-info/METADATA +298 -0
  79. codecov_cli-11.0.0.dist-info/RECORD +83 -0
  80. codecov_cli-11.0.0.dist-info/WHEEL +5 -0
  81. codecov_cli-11.0.0.dist-info/entry_points.txt +3 -0
  82. codecov_cli-11.0.0.dist-info/licenses/LICENSE +201 -0
  83. codecov_cli-11.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,97 @@
1
+ import logging
2
+ import re
3
+ from enum import Enum
4
+ from typing import Optional
5
+ from urllib.parse import urlparse
6
+
7
+ from codecov_cli.helpers.encoder import decode_slug
8
+ from codecov_cli.helpers.git_services import PullDict
9
+ from codecov_cli.helpers.git_services.github import Github
10
+
11
+ slug_regex = re.compile(r"[^/\s]+\/[^/\s]+$")
12
+
13
+ logger = logging.getLogger("codecovcli")
14
+
15
+
16
+ class GitService(Enum):
17
+ GITHUB = "github"
18
+ GITLAB = "gitlab"
19
+ BITBUCKET = "bitbucket"
20
+ GITHUB_ENTERPRISE = "github_enterprise"
21
+ GITLAB_ENTERPRISE = "gitlab_enterprise"
22
+ BITBUCKET_SERVER = "bitbucket_server"
23
+
24
+
25
+ def get_git_service(git):
26
+ if git == "github":
27
+ return Github()
28
+
29
+
30
+ def parse_slug(remote_repo_url: str):
31
+ """
32
+ Extracts a slug from git remote urls. returns None if the url is invalid
33
+
34
+ Examples:
35
+ - https://github.com/codecov/codecov-cli.git returns codecov/codecov-cli
36
+ - git@github.com:codecov/codecov-cli.git returns codecov/codecov-cli
37
+ """
38
+ parsed_url = urlparse(remote_repo_url)
39
+
40
+ path_to_parse = parsed_url.path
41
+
42
+ if path_to_parse.endswith("/"):
43
+ path_to_parse = path_to_parse.rsplit("/", 1)[0]
44
+ if path_to_parse.endswith(".git"):
45
+ path_to_parse = path_to_parse.rsplit(".git", 1)[0]
46
+ if ":" in path_to_parse:
47
+ path_to_parse = path_to_parse.split(":", 1)[1]
48
+ if path_to_parse.startswith("/"):
49
+ path_to_parse = path_to_parse[1:]
50
+
51
+ if not slug_regex.match(path_to_parse):
52
+ return None
53
+
54
+ return path_to_parse
55
+
56
+
57
+ def parse_git_service(remote_repo_url: str):
58
+ """
59
+ Extracts git service from git remote urls. returns None if the url is invalid
60
+
61
+ Possible cases we're considering:
62
+ - https://github.com/codecov/codecov-cli.git returns github
63
+ - git@github.com:codecov/codecov-cli.git returns github
64
+ - ssh://git@github.com/gitcodecov/codecov-cli returns github
65
+ - ssh://git@github.com:gitcodecov/codecov-cli returns github
66
+ - https://user-name@bitbucket.org/namespace-codecov/first_repo.git returns bitbucket
67
+ """
68
+ services = [service.value for service in GitService]
69
+ parsed_url = urlparse(remote_repo_url)
70
+ service = None
71
+
72
+ scheme = parsed_url.scheme
73
+ if scheme in ("https", "ssh"):
74
+ netloc = parsed_url.netloc
75
+ if "@" in netloc:
76
+ netloc = netloc.split("@", 1)[1]
77
+ if "." in netloc:
78
+ netloc = netloc.split(".", 1)[0]
79
+ service = netloc
80
+ elif remote_repo_url.startswith("git@"):
81
+ path = parsed_url.path
82
+ if "@" in path:
83
+ path = path.split("@", 1)[1]
84
+ if ":" in path:
85
+ path = path.split(":", 1)[0]
86
+ if "." in path:
87
+ path = path.split(".", 1)[0]
88
+ service = path
89
+
90
+ if service in services:
91
+ return service
92
+ else:
93
+ logger.warning(
94
+ f"Service not found: {service}. Possible services are {services}",
95
+ extra=dict(remote_repo_url=remote_repo_url),
96
+ )
97
+ return None
@@ -0,0 +1,14 @@
1
+ from typing import TypedDict
2
+
3
+
4
+ class CommitInfo(TypedDict):
5
+ sha: str
6
+ label: str
7
+ ref: str
8
+ slug: str
9
+
10
+
11
+ class PullDict(TypedDict):
12
+ url: str
13
+ head: CommitInfo
14
+ base: CommitInfo
@@ -0,0 +1,40 @@
1
+ import json
2
+
3
+ import requests
4
+
5
+ from codecov_cli.helpers.git_services import PullDict
6
+
7
+
8
+ class Github:
9
+ api_url = "https://api.github.com"
10
+ api_version = "2022-11-28"
11
+
12
+ def get_pull_request(self, slug, pr_number) -> PullDict:
13
+ pull_url = f"/repos/{slug}/pulls/{pr_number}"
14
+ url = self.api_url + pull_url
15
+ headers = {"X-GitHub-Api-Version": self.api_version}
16
+ response = requests.get(url, headers=headers)
17
+ if response.status_code == 200:
18
+ res = json.loads(response.text)
19
+ return {
20
+ "url": res["url"],
21
+ "head": {
22
+ "sha": res["head"]["sha"],
23
+ "label": res["head"]["label"],
24
+ "ref": res["head"]["ref"],
25
+ # Through empiric test data it seems that the "repo" key in "head" is set to None
26
+ # If the PR is from the same repo (e.g. not from a fork)
27
+ "slug": (
28
+ res["head"]["repo"]["full_name"]
29
+ if res["head"]["repo"]
30
+ else res["base"]["repo"]["full_name"]
31
+ ),
32
+ },
33
+ "base": {
34
+ "sha": res["base"]["sha"],
35
+ "label": res["base"]["label"],
36
+ "ref": res["base"]["ref"],
37
+ "slug": res["base"]["repo"]["full_name"],
38
+ },
39
+ }
40
+ return None
@@ -0,0 +1,146 @@
1
+ """
2
+ This is a copy of the function of the same name in the python
3
+ standard library. The reason for its inclusion is that it has
4
+ been added in python3.13, but not earlier versions
5
+ https://github.com/python/cpython/blob/main/Lib/glob.py
6
+ https://github.com/python/cpython/blob/main/Lib/fnmatch.py
7
+ https://github.com/python/cpython/blob/main/Lib/functools.py
8
+ """
9
+
10
+ import os
11
+ import re
12
+
13
+ from glob import fnmatch
14
+
15
+
16
+ def translate(pat, *, recursive=False, include_hidden=False, seps=None):
17
+ """Translate a pathname with shell wildcards to a regular expression.
18
+
19
+ If `recursive` is true, the pattern segment '**' will match any number of
20
+ path segments.
21
+
22
+ If `include_hidden` is true, wildcards can match path segments beginning
23
+ with a dot ('.').
24
+
25
+ If a sequence of separator characters is given to `seps`, they will be
26
+ used to split the pattern into segments and match path separators. If not
27
+ given, os.path.sep and os.path.altsep (where available) are used.
28
+ """
29
+ if not seps:
30
+ if os.path.altsep:
31
+ seps = (os.path.sep, os.path.altsep)
32
+ else:
33
+ seps = os.path.sep
34
+ escaped_seps = ''.join(map(re.escape, seps))
35
+ any_sep = f'[{escaped_seps}]' if len(seps) > 1 else escaped_seps
36
+ not_sep = f'[^{escaped_seps}]'
37
+ if include_hidden:
38
+ one_last_segment = f'{not_sep}+'
39
+ one_segment = f'{one_last_segment}{any_sep}'
40
+ any_segments = f'(?:.+{any_sep})?'
41
+ any_last_segments = '.*'
42
+ else:
43
+ one_last_segment = f'[^{escaped_seps}.]{not_sep}*'
44
+ one_segment = f'{one_last_segment}{any_sep}'
45
+ any_segments = f'(?:{one_segment})*'
46
+ any_last_segments = f'{any_segments}(?:{one_last_segment})?'
47
+
48
+ results = []
49
+ parts = re.split(any_sep, pat)
50
+ last_part_idx = len(parts) - 1
51
+ for idx, part in enumerate(parts):
52
+ if part == '*':
53
+ results.append(one_segment if idx < last_part_idx else one_last_segment)
54
+ elif recursive and part == '**':
55
+ if idx < last_part_idx:
56
+ if parts[idx + 1] != '**':
57
+ results.append(any_segments)
58
+ else:
59
+ results.append(any_last_segments)
60
+ else:
61
+ if part:
62
+ if not include_hidden and part[0] in '*?':
63
+ results.append(r'(?!\.)')
64
+ results.extend(_translate(part, f'{not_sep}*', not_sep)[0])
65
+ if idx < last_part_idx:
66
+ results.append(any_sep)
67
+ res = ''.join(results)
68
+ return fr'(?s:{res})\Z'
69
+
70
+
71
+ _re_setops_sub = re.compile(r'([&~|])').sub
72
+ def _translate(pat, star, question_mark):
73
+ res = []
74
+ add = res.append
75
+ star_indices = []
76
+
77
+ i, n = 0, len(pat)
78
+ while i < n:
79
+ c = pat[i]
80
+ i = i+1
81
+ if c == '*':
82
+ # store the position of the wildcard
83
+ star_indices.append(len(res))
84
+ add(star)
85
+ # compress consecutive `*` into one
86
+ while i < n and pat[i] == '*':
87
+ i += 1
88
+ elif c == '?':
89
+ add(question_mark)
90
+ elif c == '[':
91
+ j = i
92
+ if j < n and pat[j] == '!':
93
+ j = j+1
94
+ if j < n and pat[j] == ']':
95
+ j = j+1
96
+ while j < n and pat[j] != ']':
97
+ j = j+1
98
+ if j >= n:
99
+ add('\\[')
100
+ else:
101
+ stuff = pat[i:j]
102
+ if '-' not in stuff:
103
+ stuff = stuff.replace('\\', r'\\')
104
+ else:
105
+ chunks = []
106
+ k = i+2 if pat[i] == '!' else i+1
107
+ while True:
108
+ k = pat.find('-', k, j)
109
+ if k < 0:
110
+ break
111
+ chunks.append(pat[i:k])
112
+ i = k+1
113
+ k = k+3
114
+ chunk = pat[i:j]
115
+ if chunk:
116
+ chunks.append(chunk)
117
+ else:
118
+ chunks[-1] += '-'
119
+ # Remove empty ranges -- invalid in RE.
120
+ for k in range(len(chunks)-1, 0, -1):
121
+ if chunks[k-1][-1] > chunks[k][0]:
122
+ chunks[k-1] = chunks[k-1][:-1] + chunks[k][1:]
123
+ del chunks[k]
124
+ # Escape backslashes and hyphens for set difference (--).
125
+ # Hyphens that create ranges shouldn't be escaped.
126
+ stuff = '-'.join(s.replace('\\', r'\\').replace('-', r'\-')
127
+ for s in chunks)
128
+ i = j+1
129
+ if not stuff:
130
+ # Empty range: never match.
131
+ add('(?!)')
132
+ elif stuff == '!':
133
+ # Negated empty range: match any character.
134
+ add('.')
135
+ else:
136
+ # Escape set operations (&&, ~~ and ||).
137
+ stuff = _re_setops_sub(r'\\\1', stuff)
138
+ if stuff[0] == '!':
139
+ stuff = '^' + stuff[1:]
140
+ elif stuff[0] in ('^', '['):
141
+ stuff = '\\' + stuff
142
+ add(f'[{stuff}]')
143
+ else:
144
+ add(re.escape(c))
145
+ assert i == n
146
+ return res, star_indices
@@ -0,0 +1,77 @@
1
+ import json
2
+ import logging
3
+
4
+ import click
5
+
6
+ # Heavily inspired on https://github.com/click-contrib/click-log/blob/master/click_log/core.py
7
+
8
+
9
+ class JsonEncoder(json.JSONEncoder):
10
+ """
11
+ A custom encoder extending the default JSONEncoder
12
+ """
13
+
14
+ def default(self, obj):
15
+ try:
16
+ return super(JsonEncoder, self).default(obj)
17
+ except TypeError:
18
+ try:
19
+ return str(obj)
20
+ except Exception:
21
+ return None
22
+
23
+
24
+ class ColorFormatter(logging.Formatter):
25
+ colors = {
26
+ "error": dict(fg="red"),
27
+ "exception": dict(fg="red"),
28
+ "critical": dict(fg="red"),
29
+ "debug": dict(fg="blue"),
30
+ "warning": dict(fg="yellow"),
31
+ "info": dict(fg="blue"),
32
+ }
33
+
34
+ def format(self, record):
35
+ if not record.exc_info:
36
+ level = record.levelname.lower()
37
+ asctime = self.formatTime(record, self.datefmt)
38
+ msg = record.getMessage()
39
+ if level in self.colors:
40
+ prefix = click.style("{}".format(level), **self.colors[level])
41
+ msg = "\n".join(
42
+ f"{prefix} - {asctime} -- {x}" for x in msg.splitlines()
43
+ )
44
+ if hasattr(record, "extra_log_attributes"):
45
+ token = record.extra_log_attributes.get("token")
46
+ if token:
47
+ record.extra_log_attributes["token"] = (
48
+ "NOTOKEN" if not token else (str(token)[:1] + 18 * "*")
49
+ )
50
+ msg += " --- " + json.dumps(
51
+ record.extra_log_attributes, cls=JsonEncoder
52
+ )
53
+ return msg
54
+ return super().format(record)
55
+
56
+
57
+ class ClickHandler(logging.Handler):
58
+ _use_stderr = True
59
+ formatter = ColorFormatter()
60
+
61
+ def emit(self, record):
62
+ try:
63
+ msg = self.format(record)
64
+ click.echo(msg, err=self._use_stderr)
65
+ except Exception:
66
+ self.handleError(record)
67
+
68
+
69
+ def configure_logger(logger: logging.Logger, log_level=logging.INFO):
70
+ # This if exists to avoid an issue where extra handlers would be added by tests that use runner.invoke()
71
+ # Which would cause subsequent tests to failed due to repeated log lines
72
+ if not logger.hasHandlers():
73
+ ch = ClickHandler()
74
+ ch.setFormatter(ColorFormatter())
75
+ logger.addHandler(ch)
76
+ logger.propagate = False
77
+ logger.setLevel(log_level)
@@ -0,0 +1,51 @@
1
+ import click
2
+
3
+ from codecov_cli.fallbacks import CodecovOption, FallbackFieldEnum
4
+ from codecov_cli.helpers.git import GitService
5
+
6
+ _global_options = [
7
+ click.option(
8
+ "-C",
9
+ "--sha",
10
+ "--commit-sha",
11
+ "commit_sha",
12
+ help="Commit SHA (with 40 chars)",
13
+ cls=CodecovOption,
14
+ fallback_field=FallbackFieldEnum.commit_sha,
15
+ required=True,
16
+ ),
17
+ click.option(
18
+ "-Z",
19
+ "--fail-on-error",
20
+ "fail_on_error",
21
+ is_flag=True,
22
+ help="Exit with non-zero code in case of error",
23
+ ),
24
+ click.option(
25
+ "--git-service",
26
+ cls=CodecovOption,
27
+ fallback_field=FallbackFieldEnum.git_service,
28
+ type=click.Choice([service.value for service in GitService]),
29
+ ),
30
+ click.option(
31
+ "-t",
32
+ "--token",
33
+ help="Codecov upload token",
34
+ envvar="CODECOV_TOKEN",
35
+ ),
36
+ click.option(
37
+ "-r",
38
+ "--slug",
39
+ "slug",
40
+ cls=CodecovOption,
41
+ fallback_field=FallbackFieldEnum.slug,
42
+ help="owner/repo slug used instead of the private repo token in Self-hosted",
43
+ envvar="CODECOV_SLUG",
44
+ ),
45
+ ]
46
+
47
+
48
+ def global_options(func):
49
+ for option in reversed(_global_options):
50
+ func = option(func)
51
+ return func
@@ -0,0 +1,198 @@
1
+ import json
2
+ import logging
3
+ from sys import exit
4
+ from time import sleep
5
+ from typing import Optional
6
+
7
+ import click
8
+ import requests
9
+
10
+ from codecov_cli import __version__
11
+ from codecov_cli.types import RequestError, RequestResult
12
+
13
+ logger = logging.getLogger("codecovcli")
14
+
15
+ MAX_RETRIES = 3
16
+
17
+ USER_AGENT = f"codecov-cli/{__version__}"
18
+
19
+
20
+ def _set_user_agent(headers: Optional[dict] = None) -> dict:
21
+ headers = headers or {}
22
+ headers.setdefault("User-Agent", USER_AGENT)
23
+ return headers
24
+
25
+
26
+ def patch(url: str, headers: dict = None, json: dict = None) -> requests.Response:
27
+ headers = _set_user_agent(headers)
28
+ return requests.patch(url, json=json, headers=headers)
29
+
30
+
31
+ def get(url: str, headers: dict = None, params: dict = None) -> requests.Response:
32
+ headers = _set_user_agent(headers)
33
+ return requests.get(url, params=params, headers=headers)
34
+
35
+
36
+ def put(url: str, data: dict = None, headers: dict = None) -> requests.Response:
37
+ headers = _set_user_agent(headers)
38
+ return requests.put(url, data=data, headers=headers)
39
+
40
+
41
+ def post(
42
+ url: str,
43
+ data: Optional[dict] = None,
44
+ headers: Optional[dict] = None,
45
+ params: Optional[dict] = None,
46
+ ) -> requests.Response:
47
+ headers = _set_user_agent(headers)
48
+ return requests.post(url, json=data, headers=headers, params=params)
49
+
50
+
51
+ def backoff_time(curr_retry):
52
+ return 2 ** (curr_retry - 1)
53
+
54
+
55
+ class RetryException(Exception): ...
56
+
57
+
58
+ def retry_request(func):
59
+ def wrapper(*args, **kwargs):
60
+ retry = 0
61
+ while retry < MAX_RETRIES:
62
+ try:
63
+ response = func(*args, **kwargs)
64
+ if response.status_code >= 500:
65
+ logger.warning(
66
+ f"Response status code was {response.status_code}.",
67
+ extra=dict(extra_log_attributes=dict(retry=retry)),
68
+ )
69
+ raise RetryException
70
+ return response
71
+ except (
72
+ requests.exceptions.ConnectionError,
73
+ requests.exceptions.Timeout,
74
+ RetryException,
75
+ ):
76
+ logger.warning(
77
+ "Request failed. Retrying",
78
+ extra=dict(extra_log_attributes=dict(retry=retry)),
79
+ )
80
+ sleep(backoff_time(retry))
81
+ retry += 1
82
+ raise Exception(f"Request failed after too many retries. URL: {kwargs.get('url', args[0] if args else 'Unknown')}")
83
+
84
+ return wrapper
85
+
86
+
87
+ @retry_request
88
+ def send_post_request(
89
+ url: str,
90
+ data: Optional[dict] = None,
91
+ headers: Optional[dict] = None,
92
+ params: Optional[dict] = None,
93
+ ):
94
+ return request_result(post(url=url, data=data, headers=headers, params=params))
95
+
96
+
97
+ @retry_request
98
+ def send_get_request(
99
+ url: str, headers: dict = None, params: dict = None
100
+ ) -> RequestResult:
101
+ return request_result(get(url=url, headers=headers, params=params))
102
+
103
+
104
+ def get_token_header_or_fail(token: Optional[str]) -> dict:
105
+ """
106
+ Rejects requests with no Authorization token. Prevents tokenless uploads.
107
+ """
108
+ if token is None:
109
+ raise click.ClickException(
110
+ "Codecov token not found. Please provide Codecov token with -t flag."
111
+ )
112
+ return {"Authorization": f"token {token}"}
113
+
114
+
115
+ def get_token_header(token: Optional[str]) -> Optional[dict]:
116
+ """
117
+ Allows requests with no Authorization token.
118
+ """
119
+ if token is None:
120
+ return None
121
+ return {"Authorization": f"token {token}"}
122
+
123
+
124
+ @retry_request
125
+ def send_put_request(
126
+ url: str,
127
+ data: dict = None,
128
+ headers: dict = None,
129
+ ):
130
+ return request_result(put(url=url, data=data, headers=headers))
131
+
132
+
133
+ def request_result(resp: requests.Response) -> RequestResult:
134
+ if resp.status_code >= 400:
135
+ return RequestResult(
136
+ status_code=resp.status_code,
137
+ error=RequestError(
138
+ code=f"HTTP Error {resp.status_code}",
139
+ description=resp.text,
140
+ params={},
141
+ ),
142
+ warnings=[],
143
+ text=resp.text,
144
+ )
145
+
146
+ return RequestResult(
147
+ status_code=resp.status_code, error=None, warnings=[], text=resp.text
148
+ )
149
+
150
+
151
+ def log_warnings_and_errors_if_any(
152
+ sending_result: RequestResult, process_desc: str, fail_on_error: bool = False
153
+ ):
154
+ logger.info(
155
+ f"Process {process_desc} complete",
156
+ )
157
+ logger.debug(
158
+ f"{process_desc} result",
159
+ extra=dict(
160
+ extra_log_attributes=dict(result=_sanitize_request_result(sending_result))
161
+ ),
162
+ )
163
+ if sending_result.warnings:
164
+ number_warnings = len(sending_result.warnings)
165
+ pluralization = "s" if number_warnings > 1 else ""
166
+ logger.info(
167
+ f"{process_desc} process had {number_warnings} warning{pluralization}",
168
+ )
169
+ for ind, w in enumerate(sending_result.warnings):
170
+ logger.warning(f"Warning {ind + 1}: {w.message}")
171
+ if sending_result.error is not None:
172
+ logger.error(f"{process_desc} failed: {sending_result.error.description}")
173
+ if fail_on_error:
174
+ exit(1)
175
+
176
+
177
+ def _sanitize_request_result(result: RequestResult):
178
+ if not hasattr(result, "text"):
179
+ return result
180
+
181
+ try:
182
+ text_as_dict = json.loads(result.text)
183
+ token = text_as_dict.get("repository").get("yaml").get("codecov").get("token")
184
+ if token:
185
+ sanitized_token = str(token)[:1] + 18 * "*"
186
+ text_as_dict["repository"]["yaml"]["codecov"]["token"] = sanitized_token
187
+ sanitized_text = json.dumps(text_as_dict)
188
+
189
+ return RequestResult(
190
+ status_code=result.status_code,
191
+ error=result.error,
192
+ warnings=result.warnings,
193
+ text=sanitized_text,
194
+ )
195
+ except (AttributeError, json.JSONDecodeError):
196
+ pass
197
+
198
+ return result
@@ -0,0 +1,15 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ReportType(Enum):
5
+ COVERAGE = "coverage"
6
+ TEST_RESULTS = "test_results"
7
+
8
+
9
+ def report_type_from_str(report_type_str: str) -> ReportType:
10
+ if report_type_str == "coverage":
11
+ return ReportType.COVERAGE
12
+ elif report_type_str == "test_results":
13
+ return ReportType.TEST_RESULTS
14
+ else:
15
+ raise ValueError(f"Invalid upload type: {report_type_str}")
@@ -0,0 +1,13 @@
1
+ import re
2
+
3
+ import click
4
+
5
+
6
+ def validate_commit_sha(ctx, param, value):
7
+ if value == "" or value is None:
8
+ raise click.MissingParameter()
9
+ if len(value) < 40:
10
+ raise click.BadParameter("Use the full commit SHA")
11
+ if not re.match(r"[0-9a-f]{40}", value):
12
+ raise click.BadParameter("Commit SHA doesn't match SHA1 regex")
13
+ return value