suite-py 1.40.3__tar.gz → 1.41.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.
Files changed (53) hide show
  1. {suite_py-1.40.3 → suite_py-1.41.1}/PKG-INFO +3 -3
  2. {suite_py-1.40.3 → suite_py-1.41.1}/pyproject.toml +3 -3
  3. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/__version__.py +1 -1
  4. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/cli.py +13 -8
  5. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/aggregator.py +2 -0
  6. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/ask_review.py +3 -0
  7. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/batch_job.py +2 -0
  8. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/bump.py +3 -0
  9. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/check.py +2 -0
  10. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/create_branch.py +2 -0
  11. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/deploy.py +2 -0
  12. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/docker.py +2 -0
  13. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/generator.py +3 -0
  14. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/id.py +2 -0
  15. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/ip.py +2 -0
  16. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/login.py +2 -0
  17. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/merge_pr.py +2 -0
  18. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/open_pr.py +2 -0
  19. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/project_lock.py +2 -0
  20. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/qa.py +22 -12
  21. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/release.py +2 -0
  22. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/secret.py +2 -0
  23. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/set_token.py +2 -0
  24. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/status.py +2 -0
  25. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/captainhook_handler.py +22 -11
  26. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/drone_handler.py +3 -3
  27. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/git_handler.py +9 -9
  28. suite_py-1.41.1/suite_py/lib/handler/metrics_handler.py +84 -0
  29. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/vault_handler.py +1 -1
  30. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/version_handler.py +1 -1
  31. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/logger.py +5 -4
  32. suite_py-1.41.1/suite_py/lib/metrics.py +54 -0
  33. {suite_py-1.40.3 → suite_py-1.41.1}/LICENSE-APACHE +0 -0
  34. {suite_py-1.40.3 → suite_py-1.41.1}/LICENSE-MIT +0 -0
  35. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/__init__.py +0 -0
  36. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/__init__.py +0 -0
  37. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/common.py +0 -0
  38. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/commands/templates/login.html +0 -0
  39. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/__init__.py +0 -0
  40. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/config.py +0 -0
  41. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/__init__.py +0 -0
  42. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/aws_handler.py +0 -0
  43. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/changelog_handler.py +0 -0
  44. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/frequent_reviewers_handler.py +0 -0
  45. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/github_handler.py +0 -0
  46. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/prompt_utils.py +0 -0
  47. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/qainit_handler.py +0 -0
  48. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/handler/youtrack_handler.py +0 -0
  49. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/requests/__init__.py +0 -0
  50. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/requests/auth.py +0 -0
  51. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/requests/session.py +0 -0
  52. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/symbol.py +0 -0
  53. {suite_py-1.40.3 → suite_py-1.41.1}/suite_py/lib/tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: suite-py
3
- Version: 1.40.3
3
+ Version: 1.41.1
4
4
  Summary:
5
5
  Author: larrywax, EugenioLaghi, michelangelomo
6
6
  Author-email: devops@prima.it
@@ -19,7 +19,7 @@ Requires-Dist: PyGithub (>=1.57)
19
19
  Requires-Dist: PyYaml (>=5.4)
20
20
  Requires-Dist: Werkzeug (==2.0.2)
21
21
  Requires-Dist: autoupgrade-prima (>=0.6)
22
- Requires-Dist: black (>=22.6.0,<23.0.0)
22
+ Requires-Dist: black (>=22.6,<25.0)
23
23
  Requires-Dist: boto3 (>=1.17.84)
24
24
  Requires-Dist: cement (>=3.0)
25
25
  Requires-Dist: colorama (>=0.4.3)
@@ -38,7 +38,7 @@ Requires-Dist: pytest (>=7.0.0)
38
38
  Requires-Dist: python-dateutil (>=2.8.2)
39
39
  Requires-Dist: requests (>=2.26.0)
40
40
  Requires-Dist: requests-toolbelt (>=0.9.1)
41
- Requires-Dist: rich (==13.7.0)
41
+ Requires-Dist: rich (==13.7.1)
42
42
  Requires-Dist: semver (>=2.13.0,<3.0.0)
43
43
  Requires-Dist: termcolor (>=1.1.0)
44
44
  Requires-Dist: truststore (>=0.7,<0.9) ; python_version >= "3.10"
@@ -2,7 +2,7 @@
2
2
  authors = ["larrywax, EugenioLaghi, michelangelomo <devops@prima.it>"]
3
3
  description = ""
4
4
  name = "suite-py"
5
- version = "1.40.3"
5
+ version = "1.41.1"
6
6
 
7
7
  [tool.poetry.dependencies]
8
8
  Click = ">=7.0"
@@ -13,7 +13,7 @@ PyGithub = ">=1.57"
13
13
  PyYaml = ">=5.4"
14
14
  Werkzeug = "==2.0.2"
15
15
  autoupgrade-prima = ">=0.6"
16
- black = "^22.6.0"
16
+ black = ">=22.6,<25.0"
17
17
  boto3 = ">=1.17.84"
18
18
  cement = ">=3.0"
19
19
  colorama = ">=0.4.3"
@@ -32,7 +32,7 @@ python = ">=3.8,<4.0"
32
32
  python-dateutil = ">=2.8.2"
33
33
  requests = ">=2.26.0"
34
34
  requests-toolbelt = ">=0.9.1"
35
- rich = "==13.7.0"
35
+ rich = "==13.7.1"
36
36
  semver = "^2.13.0"
37
37
  termcolor = ">=1.1.0"
38
38
  truststore = {version = ">=0.7,<0.9", python = ">=3.10"}
@@ -1,2 +1,2 @@
1
1
  # -*- encoding: utf-8 -*-
2
- __version__ = "1.40.2"
2
+ __version__ = "1.41.1"
@@ -4,7 +4,6 @@
4
4
  # for performance reasons, so turn off the lint warning
5
5
  # pylint: disable=import-outside-toplevel
6
6
 
7
- import logging
8
7
  import os
9
8
  import sys
10
9
  from typing import Optional
@@ -16,6 +15,7 @@ from autoupgrade import Package
16
15
 
17
16
  from suite_py.__version__ import __version__
18
17
  from suite_py.lib import logger
18
+ from suite_py.lib import metrics
19
19
  from suite_py.lib.config import Config
20
20
  from suite_py.lib.handler import git_handler as git
21
21
  from suite_py.lib.handler import prompt_utils
@@ -99,11 +99,14 @@ def upgrade_suite_py_if_needed(break_on_missing_package: bool = False) -> None:
99
99
  @click.option("-v", "--verbose", count=True)
100
100
  @click.pass_context
101
101
  def main(ctx, project, timeout, verbose):
102
+ config = Config()
103
+
104
+ logger.setup(verbose)
105
+ metrics.setup(config)
106
+
107
+ logger.debug(f"v{__version__}")
102
108
  maybe_inject_truststore()
103
109
  upgrade_suite_py_if_needed(break_on_missing_package=False)
104
- print(f"v{__version__}")
105
-
106
- config = Config()
107
110
 
108
111
  if ctx.invoked_subcommand not in ALLOW_NO_GIT_SUBCOMMAND:
109
112
  project = git.get_root_folder(project)
@@ -135,10 +138,6 @@ def main(ctx, project, timeout, verbose):
135
138
  ):
136
139
  sys.exit()
137
140
 
138
- if verbose:
139
- logger.setLevel(logging.DEBUG)
140
- logger.debug("Logging as 'DEBUG'")
141
-
142
141
  ctx.ensure_object(dict)
143
142
  ctx.obj["project"] = os.path.basename(project)
144
143
  if timeout:
@@ -154,6 +153,12 @@ def main(ctx, project, timeout, verbose):
154
153
  os.chdir(os.path.join(config.user["projects_home"], ctx.obj["project"]))
155
154
 
156
155
 
156
+ @main.result_callback()
157
+ @click.pass_obj
158
+ def cleanup(_obj, _, **_kwargs):
159
+ metrics.async_upload()
160
+
161
+
157
162
  @main.command("bump", help="Bumps the project version based on the .versions.yml file")
158
163
  @click.option("--project", required=False, type=str)
159
164
  @click.option(
@@ -8,6 +8,7 @@ from rich.console import Console
8
8
  from rich.table import Table
9
9
 
10
10
  from suite_py.lib import logger
11
+ from suite_py.lib import metrics
11
12
  from suite_py.lib.handler import prompt_utils
12
13
  from suite_py.lib.handler.captainhook_handler import CaptainHook
13
14
 
@@ -17,6 +18,7 @@ class Aggregator:
17
18
  self._captainhook = CaptainHook(config)
18
19
  self._command = command
19
20
 
21
+ @metrics.command("aggregator")
20
22
  def run(self):
21
23
  if self._command == "list":
22
24
  self._list_aggregators()
@@ -2,6 +2,7 @@
2
2
  import sys
3
3
 
4
4
  from suite_py.lib import logger
5
+ from suite_py.lib import metrics
5
6
  from suite_py.lib.handler.captainhook_handler import CaptainHook
6
7
  from suite_py.lib.handler.frequent_reviewers_handler import FrequentReviewersHandler
7
8
  from suite_py.lib.handler.git_handler import GitHandler
@@ -20,7 +21,9 @@ class AskReview:
20
21
  self._github = GithubHandler(tokens)
21
22
  self._frequent_reviewers = FrequentReviewersHandler(config)
22
23
 
24
+ @metrics.command("ask-review")
23
25
  def run(self):
26
+
24
27
  users = self._maybe_get_users_list()
25
28
  pr = self._get_pr()
26
29
  youtrack_reviewers = self._ask_reviewer(users)
@@ -6,6 +6,7 @@ import textwrap
6
6
  import semver
7
7
 
8
8
  from suite_py.lib import logger
9
+ from suite_py.lib import metrics
9
10
  from suite_py.lib.handler import prompt_utils
10
11
  from suite_py.lib.handler.drone_handler import DroneHandler
11
12
  from suite_py.lib.handler.git_handler import GitHandler
@@ -28,6 +29,7 @@ class BatchJob:
28
29
  self._countries = _parse_available_countries(self._drone)
29
30
  self._repo = self._github.get_repo(project)
30
31
 
32
+ @metrics.command("batch-job")
31
33
  def run(self):
32
34
  country = prompt_utils.ask_choices(
33
35
  "Which country do you want to run the job on?",
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional, cast
9
9
  from yaml import safe_load
10
10
 
11
11
  from suite_py.lib import logger
12
+ from suite_py.lib import metrics
12
13
  from suite_py.lib.handler.changelog_handler import ChangelogHandler
13
14
  from suite_py.lib.handler.git_handler import GitHandler
14
15
  from suite_py.lib.handler.github_handler import GithubHandler
@@ -28,7 +29,9 @@ class Bump:
28
29
  _git = GitHandler(project, config)
29
30
  self._version = VersionHandler(_repo=self._repo, _git=_git, _github=_github)
30
31
 
32
+ @metrics.command("bump")
31
33
  def run(self, project: Optional[str] = None, version: Optional[str] = None) -> None:
34
+
32
35
  configurations = read_bump_configs_from_yaml(Path(".versions.yml"))
33
36
  version_config = (
34
37
  configurations[project] if project else configurations["base_project"]
@@ -1,6 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  from suite_py.lib import logger
4
+ from suite_py.lib import metrics
4
5
  from suite_py.lib.handler import prompt_utils
5
6
  from suite_py.lib.handler.captainhook_handler import CaptainHook
6
7
  from suite_py.lib.handler.drone_handler import DroneHandler
@@ -27,6 +28,7 @@ class Check:
27
28
  ("AWS", self._check_aws),
28
29
  ]
29
30
 
31
+ @metrics.command("check")
30
32
  def run(self):
31
33
  # Services
32
34
  for service, check in self._checks:
@@ -5,6 +5,7 @@ import sys
5
5
  import requests
6
6
 
7
7
  from suite_py.lib import logger
8
+ from suite_py.lib import metrics
8
9
  from suite_py.lib.handler import prompt_utils
9
10
  from suite_py.lib.handler.git_handler import GitHandler, is_branch_name_valid
10
11
  from suite_py.lib.handler.youtrack_handler import YoutrackHandler
@@ -18,6 +19,7 @@ class CreateBranch:
18
19
  self._youtrack = YoutrackHandler(config, tokens)
19
20
  self._git = GitHandler(project, config)
20
21
 
22
+ @metrics.command("create-branch")
21
23
  def run(self):
22
24
  if not self._git.is_detached() and self._git.is_dirty():
23
25
  # Default behaviour is to pull when not detached.
@@ -5,6 +5,7 @@ import sys
5
5
  from suite_py.commands import common
6
6
  from suite_py.commands.release import _parse_available_countries
7
7
  from suite_py.lib import logger
8
+ from suite_py.lib import metrics
8
9
  from suite_py.lib.handler import git_handler as git
9
10
  from suite_py.lib.handler import prompt_utils
10
11
  from suite_py.lib.handler.captainhook_handler import CaptainHook
@@ -31,6 +32,7 @@ class Deploy:
31
32
  self._countries = _parse_available_countries(self._drone)
32
33
  self._version = VersionHandler(self._repo, self._git, self._github)
33
34
 
35
+ @metrics.command("deploy")
34
36
  def run(self):
35
37
  self._stop_if_prod_locked()
36
38
 
@@ -4,6 +4,7 @@ import sys
4
4
  from halo import Halo
5
5
 
6
6
  from suite_py.lib import logger
7
+ from suite_py.lib import metrics
7
8
  from suite_py.lib.handler import prompt_utils
8
9
  from suite_py.lib.handler.drone_handler import DroneHandler
9
10
  from suite_py.lib.handler.git_handler import GitHandler
@@ -23,6 +24,7 @@ class Docker:
23
24
  self._git = GitHandler(project, config)
24
25
  self._drone = DroneHandler(config, tokens, repo=project)
25
26
 
27
+ @metrics.command("docker")
26
28
  def run(self):
27
29
  if self._project != "docker":
28
30
  logger.error("`suite-py docker` must run inside docker repository.")
@@ -10,6 +10,7 @@ import yaml
10
10
  from jinja2 import DebugUndefined, Environment, FileSystemLoader
11
11
 
12
12
  from suite_py.lib import logger
13
+ from suite_py.lib import metrics
13
14
  from suite_py.lib.handler import prompt_utils
14
15
  from suite_py.lib.handler.drone_handler import DroneHandler
15
16
  from suite_py.lib.handler.git_handler import GitHandler
@@ -30,7 +31,9 @@ class Generator:
30
31
  )
31
32
  self._github = GithubHandler(tokens)
32
33
 
34
+ @metrics.command("generator")
33
35
  def run(self):
36
+
34
37
  logger.warning(
35
38
  "This command requires shellcheck and shfmt, please install these tools before continuing"
36
39
  )
@@ -4,6 +4,7 @@ import re
4
4
  from pptree import Node, print_tree
5
5
 
6
6
  from suite_py.lib import logger
7
+ from suite_py.lib import metrics
7
8
  from suite_py.lib.handler.aws_handler import Aws
8
9
 
9
10
 
@@ -13,6 +14,7 @@ class ID:
13
14
  self._env = env
14
15
  self._aws = Aws(config)
15
16
 
17
+ @metrics.command("id")
16
18
  def run(self):
17
19
 
18
20
  clusters_names = self._aws.get_ecs_clusters(self._env)
@@ -5,6 +5,7 @@ import re
5
5
  from pptree import Node, print_tree
6
6
 
7
7
  from suite_py.lib import logger
8
+ from suite_py.lib import metrics
8
9
  from suite_py.lib.handler.aws_handler import Aws
9
10
 
10
11
 
@@ -14,6 +15,7 @@ class IP:
14
15
  self._env = env
15
16
  self._aws = Aws(config)
16
17
 
18
+ @metrics.command("ip")
17
19
  def run(self):
18
20
 
19
21
  clusters_names = self._aws.get_ecs_clusters(self._env)
@@ -13,6 +13,7 @@ from flask import Flask, render_template, request
13
13
  from werkzeug.serving import make_server
14
14
 
15
15
  from suite_py.lib import logger
16
+ from suite_py.lib import metrics
16
17
 
17
18
  # Global vars for comunication between flask thread and main cli function
18
19
  received_callback = None
@@ -99,6 +100,7 @@ class Login:
99
100
  def generate_challenge(self, a_verifier):
100
101
  return self.url_encode_no_padding(hashlib.sha256(a_verifier.encode()).digest())
101
102
 
103
+ @metrics.command("login")
102
104
  def run(self):
103
105
  global received_callback
104
106
 
@@ -4,6 +4,7 @@ import sys
4
4
  from halo import Halo
5
5
 
6
6
  from suite_py.lib import logger
7
+ from suite_py.lib import metrics
7
8
  from suite_py.lib.handler import git_handler as git
8
9
  from suite_py.lib.handler import prompt_utils
9
10
  from suite_py.lib.handler.captainhook_handler import CaptainHook
@@ -24,6 +25,7 @@ class MergePR:
24
25
  self._github = GithubHandler(tokens)
25
26
  self._drone = DroneHandler(config, tokens, repo=project)
26
27
 
28
+ @metrics.command("merge-pr")
27
29
  def run(self):
28
30
  # pylint: disable=too-many-branches
29
31
  self._stop_if_master_locked()
@@ -5,6 +5,7 @@ from github import GithubException
5
5
 
6
6
  from suite_py.commands.ask_review import AskReview
7
7
  from suite_py.lib import logger
8
+ from suite_py.lib import metrics
8
9
  from suite_py.lib.handler import prompt_utils
9
10
  from suite_py.lib.handler.git_handler import GitHandler, get_commit_logs
10
11
  from suite_py.lib.handler.github_handler import GithubHandler
@@ -21,6 +22,7 @@ class OpenPR:
21
22
  self._branch_name = self._git.current_branch_name()
22
23
  self._github = GithubHandler(tokens)
23
24
 
25
+ @metrics.command("open-pr")
24
26
  def run(self):
25
27
  if not self._git.remote_branch_exists(self._branch_name):
26
28
  logger.warning(f"No branch named {self._branch_name} found on GitHub")
@@ -4,6 +4,7 @@ import sys
4
4
  import requests
5
5
 
6
6
  from suite_py.lib import logger
7
+ from suite_py.lib import metrics
7
8
  from suite_py.lib.handler.captainhook_handler import CaptainHook
8
9
 
9
10
 
@@ -14,6 +15,7 @@ class ProjectLock:
14
15
  self._action = action
15
16
  self._captainhook = CaptainHook(config, tokens=tokens)
16
17
 
18
+ @metrics.command("manage-project-lock")
17
19
  def run(self):
18
20
  if self._action == "lock":
19
21
  try:
@@ -12,6 +12,7 @@ from rich.table import Table
12
12
 
13
13
  from suite_py.commands.login import Login
14
14
  from suite_py.lib import logger
15
+ from suite_py.lib import metrics
15
16
  from suite_py.lib.handler import git_handler as git
16
17
  from suite_py.lib.handler import prompt_utils
17
18
  from suite_py.lib.handler.drone_handler import DroneHandler
@@ -32,6 +33,7 @@ class QA:
32
33
  self._youtrack = YoutrackHandler(config, tokens)
33
34
  self._drone = DroneHandler(config, tokens)
34
35
 
36
+ @metrics.command("qa")
35
37
  def run(self):
36
38
  if not self._qainit.user_info():
37
39
  logger.warning("You're not logged in.")
@@ -96,15 +98,21 @@ class QA:
96
98
  qa["name"],
97
99
  qa["hash"],
98
100
  qa["card"],
99
- qa.get("created", {}).get("github_username", "/")
100
- if qa["created"] is not None
101
- else "/",
102
- qa.get("updated", {}).get("github_username", "/")
103
- if qa["updated"] is not None
104
- else "/",
105
- qa.get("deleted", {}).get("github_username", "/")
106
- if qa["deleted"] is not None
107
- else "/",
101
+ (
102
+ qa.get("created", {}).get("github_username", "/")
103
+ if qa["created"] is not None
104
+ else "/"
105
+ ),
106
+ (
107
+ qa.get("updated", {}).get("github_username", "/")
108
+ if qa["updated"] is not None
109
+ else "/"
110
+ ),
111
+ (
112
+ qa.get("deleted", {}).get("github_username", "/")
113
+ if qa["deleted"] is not None
114
+ else "/"
115
+ ),
108
116
  self._clean_date(qa["updated_at"]),
109
117
  qa["status"],
110
118
  )
@@ -211,9 +219,11 @@ class QA:
211
219
  table.add_row(
212
220
  resource["name"],
213
221
  drone_url,
214
- resource["ref"]
215
- if resource["ref"] == "master"
216
- else f"[green]{resource['ref']}[/green]",
222
+ (
223
+ resource["ref"]
224
+ if resource["ref"] == "master"
225
+ else f"[green]{resource['ref']}[/green]"
226
+ ),
217
227
  self._clean_date(resource["updated_at"]),
218
228
  resource["status"],
219
229
  )
@@ -7,6 +7,7 @@ from halo import Halo
7
7
 
8
8
  from suite_py.commands import common
9
9
  from suite_py.lib import logger
10
+ from suite_py.lib import metrics
10
11
  from suite_py.lib.handler import git_handler as git
11
12
  from suite_py.lib.handler import prompt_utils
12
13
  from suite_py.lib.handler.captainhook_handler import CaptainHook
@@ -35,6 +36,7 @@ class Release:
35
36
  self._drone = DroneHandler(config, tokens, repo=project)
36
37
  self._version = VersionHandler(self._repo, self._git, self._github)
37
38
 
39
+ @metrics.command("release")
38
40
  def run(self):
39
41
  self._stop_if_prod_locked()
40
42
  self._git.fetch()
@@ -6,6 +6,7 @@ import sys
6
6
  import yaml
7
7
 
8
8
  from suite_py.lib import logger
9
+ from suite_py.lib import metrics
9
10
  from suite_py.lib.handler import prompt_utils
10
11
  from suite_py.lib.handler.vault_handler import VaultHandler
11
12
 
@@ -43,6 +44,7 @@ class Secret:
43
44
  self._base_profile = base_profile
44
45
  self._secret_file = secret_file
45
46
 
47
+ @metrics.command("secret")
46
48
  def run(self):
47
49
  if self._base_profile is not None:
48
50
  self._config.vault["base_secret_profile"] = self._base_profile
@@ -1,6 +1,7 @@
1
1
  import sys
2
2
 
3
3
  from suite_py.lib import logger
4
+ from suite_py.lib import metrics
4
5
  from suite_py.lib.handler import prompt_utils
5
6
  from suite_py.lib.tokens import Tokens
6
7
 
@@ -9,6 +10,7 @@ class SetToken:
9
10
  def __init__(self, tokens: Tokens):
10
11
  self._tokens = tokens
11
12
 
13
+ @metrics.command("set-token")
12
14
  def run(self):
13
15
  selected = self._select_token()
14
16
  if not selected or selected.lower() != selected or selected.strip() != selected:
@@ -2,6 +2,7 @@
2
2
  from halo import Halo
3
3
 
4
4
  from suite_py.lib import logger
5
+ from suite_py.lib import metrics
5
6
  from suite_py.lib.handler.captainhook_handler import CaptainHook
6
7
  from suite_py.lib.symbol import CHECKMARK, CROSSMARK
7
8
 
@@ -11,6 +12,7 @@ class Status:
11
12
  self._project = project
12
13
  self._captainhook = CaptainHook(config)
13
14
 
15
+ @metrics.command("status")
14
16
  def run(self):
15
17
  with Halo(text="Contacting Captainhook...", spinner="dots", color="magenta"):
16
18
  staging_status = self._captainhook.status(self._project, "staging").json()
@@ -56,33 +56,42 @@ class CaptainHook:
56
56
  data = {"aggregator": aggregator, "qa_address": qa_address}
57
57
  return self.send_put_request("/cloudflare/aggregators", data)
58
58
 
59
- def send_post_request(self, endpoint, data):
59
+ def send_metrics(self, metrics):
60
+ self.send_post_request("/suite_py/metrics/", json=metrics).raise_for_status()
61
+
62
+ def send_post_request(self, endpoint, data=None, json=None):
60
63
  try:
61
64
  r = requests.post(
62
65
  f"{self._baseurl}{endpoint}",
63
66
  headers=self._headers,
64
67
  data=data,
68
+ json=json,
65
69
  timeout=self._timeout,
66
70
  )
67
71
 
68
72
  return self._response(r)
69
- except Exception:
70
- logger.error("Unable to contact Captainhook, are you using the VPN?")
71
- sys.exit(-1)
73
+ except Exception as e:
74
+ logger.error(
75
+ "Unable to contact Captainhook, are you logged in/using the VPN?"
76
+ )
77
+ raise e
72
78
 
73
- def send_put_request(self, endpoint, data):
79
+ def send_put_request(self, endpoint, data=None, json=None):
74
80
  try:
75
81
  r = requests.put(
76
82
  f"{self._baseurl}{endpoint}",
77
83
  headers=self._headers,
78
84
  data=data,
85
+ json=json,
79
86
  timeout=self._timeout,
80
87
  )
81
88
 
82
89
  return self._response(r)
83
- except Exception:
84
- logger.error("Unable to contact Captainhook, are you using the VPN?")
85
- sys.exit(-1)
90
+ except Exception as e:
91
+ logger.error(
92
+ "Unable to contact Captainhook, are you logged in/using the VPN?"
93
+ )
94
+ raise e
86
95
 
87
96
  def send_get_request(self, endpoint):
88
97
  try:
@@ -93,9 +102,11 @@ class CaptainHook:
93
102
  )
94
103
 
95
104
  return self._response(r)
96
- except Exception:
97
- logger.error("Unable to contact Captainhook, are you using the VPN?")
98
- sys.exit(-1)
105
+ except Exception as e:
106
+ logger.error(
107
+ "Unable to contact Captainhook, are you logged in/using the VPN?"
108
+ )
109
+ raise e
99
110
 
100
111
  def _response(self, response):
101
112
  if response.status_code == 401:
@@ -217,7 +217,7 @@ class DroneHandler:
217
217
  subprocess.run(
218
218
  ["drone", "fmt", "--save", ".drone.yml"], cwd=self._path, check=True
219
219
  )
220
- except (CalledProcessError) as e:
220
+ except CalledProcessError as e:
221
221
  logger.error(f"{self._repo}: unable to format the .drone.yml {e}")
222
222
  sys.exit(-1)
223
223
 
@@ -226,7 +226,7 @@ class DroneHandler:
226
226
  subprocess.run(
227
227
  ["drone", "lint", "--trusted", ".drone.yml"], cwd=self._path, check=True
228
228
  )
229
- except (CalledProcessError) as e:
229
+ except CalledProcessError as e:
230
230
  logger.error(f"{self._path}: the .drone.yml is not valid {e}")
231
231
  sys.exit(-1)
232
232
 
@@ -237,7 +237,7 @@ class DroneHandler:
237
237
  cwd=self._path,
238
238
  check=True,
239
239
  )
240
- except (CalledProcessError) as e:
240
+ except CalledProcessError as e:
241
241
  logger.error(f"{self._repo}: unable to sign the .drone.yml {e}")
242
242
  sys.exit(-1)
243
243
 
@@ -52,7 +52,7 @@ class GitHandler:
52
52
  )
53
53
  if not self.is_detached(): # Skip pulling if detached
54
54
  self.pull(rebase=True)
55
- except (CalledProcessError) as e:
55
+ except CalledProcessError as e:
56
56
  logger.error(f"Error during command execution: {e}")
57
57
  sys.exit(-1)
58
58
 
@@ -74,14 +74,14 @@ class GitHandler:
74
74
  stdout=subprocess.DEVNULL,
75
75
  stderr=subprocess.DEVNULL,
76
76
  )
77
- except (CalledProcessError) as e:
77
+ except CalledProcessError as e:
78
78
  logger.error(f"Error during command execution: {e}")
79
79
  sys.exit(-1)
80
80
 
81
81
  def add(self):
82
82
  try:
83
83
  subprocess.run(["git", "add", "."], cwd=self._path, check=True)
84
- except (CalledProcessError) as e:
84
+ except CalledProcessError as e:
85
85
  logger.error(f"Error during command execution: {e}")
86
86
  sys.exit(-1)
87
87
 
@@ -95,7 +95,7 @@ class GitHandler:
95
95
  stdout=subprocess.DEVNULL,
96
96
  stderr=subprocess.DEVNULL,
97
97
  )
98
- except (CalledProcessError) as e:
98
+ except CalledProcessError as e:
99
99
  logger.error(f"Error during command execution: {e}")
100
100
  sys.exit(-1)
101
101
 
@@ -118,7 +118,7 @@ class GitHandler:
118
118
  stdout=subprocess.DEVNULL,
119
119
  stderr=subprocess.DEVNULL,
120
120
  )
121
- except (CalledProcessError) as e:
121
+ except CalledProcessError as e:
122
122
  logger.error(f"Error during command execution: {e}")
123
123
  sys.exit(-1)
124
124
 
@@ -139,7 +139,7 @@ class GitHandler:
139
139
  stdout=subprocess.DEVNULL,
140
140
  stderr=subprocess.DEVNULL,
141
141
  )
142
- except (CalledProcessError) as e:
142
+ except CalledProcessError as e:
143
143
  logger.error(f"Error during command execution: {e}")
144
144
  sys.exit(-1)
145
145
 
@@ -153,7 +153,7 @@ class GitHandler:
153
153
  stdout=subprocess.DEVNULL,
154
154
  stderr=subprocess.DEVNULL,
155
155
  )
156
- except (CalledProcessError) as e:
156
+ except CalledProcessError as e:
157
157
  logger.error(f"Error during command execution: {e}")
158
158
  sys.exit(-1)
159
159
 
@@ -232,7 +232,7 @@ class GitHandler:
232
232
  check=True,
233
233
  ).stdout.decode("utf-8")
234
234
  return re.sub("\n", "", output)
235
- except (CalledProcessError) as e:
235
+ except CalledProcessError as e:
236
236
  logger.error(f"Error during command execution: {e}")
237
237
  sys.exit(-1)
238
238
 
@@ -254,7 +254,7 @@ class GitHandler:
254
254
  ).stdout.decode("utf-8")
255
255
 
256
256
  return len(output) != 0
257
- except (CalledProcessError) as e:
257
+ except CalledProcessError as e:
258
258
  logger.error(f"Error during command execution: {e}")
259
259
  sys.exit(-1)
260
260
 
@@ -0,0 +1,84 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ import multiprocessing
4
+ import os
5
+ import platform
6
+
7
+ from suite_py.lib import logger
8
+ from suite_py.lib.config import Config
9
+ from suite_py.lib.handler.captainhook_handler import CaptainHook
10
+
11
+
12
+ # Collects metrics and sends them to datadog through captainhook
13
+ class Metrics:
14
+ def __init__(self, config: Config):
15
+ self._config = config
16
+
17
+ # Emits the command_executed metric
18
+ def command_executed(self, command: str, success: None | bool = True):
19
+ metric_data = {
20
+ "type": "command_executed",
21
+ "os": platform.system(),
22
+ "command": command,
23
+ "success": success,
24
+ }
25
+
26
+ self._create_metric(metric_data)
27
+
28
+ def _create_metric(self, metric):
29
+ # Instead of trying to submit metrics all at once first we save them to a file first
30
+ # That way we can batch metric submissions and retry them if they fail (eg. because the user isn't authenticated with okta)
31
+ metrics = self._config.get_cookie("metrics", [])
32
+ if not isinstance(metrics, list):
33
+ logger.warning(
34
+ f"Metrics cookie is not a list! Replacing {metrics} with an empty list"
35
+ )
36
+ metrics = []
37
+
38
+ logger.debug(f"creating metric: {metric}")
39
+ metrics.append(metric)
40
+ self._config.put_cookie("metrics", metrics)
41
+
42
+ # Upload metrics in a detached background process
43
+ def async_upload(self):
44
+ # Double fork to detach the process
45
+ def child():
46
+ if (
47
+ # Windows doesn't support forking.
48
+ # There aren't any windows users that we know of so we can just block application exit
49
+ # If windows users appear we should fix this
50
+ os.name == "posix"
51
+ and
52
+ # Forking here is safe since we are running in a fresh process spawned by multiprocessing
53
+ os.fork() != 0
54
+ ):
55
+ return
56
+
57
+ self.upload()
58
+
59
+ # Fall back to a sync upload if async fails
60
+ try:
61
+ p = multiprocessing.Process(target=child)
62
+ p.start()
63
+ p.join()
64
+ except Exception:
65
+ logger.debug(
66
+ "Error uploading metrics asynchronously, trying sync:", exc_info=True
67
+ )
68
+ self.upload()
69
+
70
+ def upload(self):
71
+ captainhook = CaptainHook(self._config)
72
+
73
+ metrics = self._config.get_cookie("metrics", [])
74
+ # Prevent double uploads
75
+ #
76
+ # This isn't atomic and could still lead to the same metrics being uploaded twice
77
+ # but this occuring is unlikely enough that it shouldn't significantly influence our stats
78
+ self._config.put_cookie("metrics", [])
79
+ try:
80
+ if len(metrics) != 0:
81
+ captainhook.send_metrics(metrics)
82
+ except Exception:
83
+ # Upload failed, try again later
84
+ self._config.put_cookie("metrics", metrics)
@@ -23,6 +23,6 @@ class VaultHandler:
23
23
  ) # .stdout.read()
24
24
  c.wait()
25
25
  return c
26
- except (CalledProcessError) as e:
26
+ except CalledProcessError as e:
27
27
  logger.error(f"Error during command execution: {e}")
28
28
  sys.exit(-1)
@@ -1,5 +1,5 @@
1
1
  import dataclasses
2
- from typing import Dict, List, Optional, Literal
2
+ from typing import Dict, List, Literal, Optional
3
3
 
4
4
  import semver
5
5
  from halo import Halo
@@ -1,18 +1,19 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  import logging
3
-
4
3
  import logzero
5
4
  from logzero import logger as _logger
6
5
 
7
6
  DEFAULT_FORMAT = "%(color)s[%(levelname)1.4s]%(end_color)s %(message)s"
8
7
  # Set a custom formatter
9
8
  formatter = logzero.LogFormatter(fmt=DEFAULT_FORMAT)
10
- logzero.setup_default_logger(formatter=formatter)
11
- _logger.setLevel(logging.INFO)
12
9
 
13
10
 
14
- def setLevel(level):
11
+ def setup(verbose):
12
+ logzero.setup_default_logger(formatter=formatter)
13
+ level = logging.DEBUG if verbose else logging.INFO
14
+
15
15
  _logger.setLevel(level)
16
+ _logger.debug("Logging as %s", level)
16
17
 
17
18
 
18
19
  def debug(message, *args, **kwargs):
@@ -0,0 +1,54 @@
1
+ # -*- encoding: utf-8 -*-
2
+ import functools
3
+ from suite_py.lib.config import Config
4
+ from suite_py.lib.handler.metrics_handler import Metrics
5
+
6
+ _metrics_handler = None
7
+
8
+
9
+ def _metrics() -> Metrics:
10
+ if _metrics_handler:
11
+ return _metrics_handler
12
+
13
+ raise Exception(
14
+ "command_executed called before logger.setup(). This is a bug, please report it"
15
+ )
16
+
17
+
18
+ def setup(config: Config):
19
+ global _metrics_handler
20
+ _metrics_handler = Metrics(config)
21
+
22
+
23
+ def command_executed(command):
24
+ _metrics().command_executed(command)
25
+
26
+
27
+ def async_upload():
28
+ _metrics().async_upload()
29
+
30
+
31
+ # Decorator that emits the command_executed metric with the given command name,
32
+ # and sets the success paramter to false if the function exited by throwing an error
33
+ def command(command: str):
34
+ def decorator(func):
35
+ @functools.wraps(func)
36
+ def wrapper(*args, **kwargs):
37
+ try:
38
+ res = func(*args, **kwargs)
39
+ _metrics().command_executed(command, success=True)
40
+ return res
41
+ except Exception as e:
42
+ _metrics().command_executed(command, success=False)
43
+ raise e
44
+ # We use sys.exit everywhere, report those
45
+ except SystemExit as e:
46
+ # Report sys.exit(0) and sys.exit() as successes
47
+ success = e.code in (0, None)
48
+ _metrics().command_executed(command, success=success)
49
+
50
+ raise e
51
+
52
+ return wrapper
53
+
54
+ return decorator
File without changes
File without changes