suite-py 1.44.0__tar.gz → 1.46.0__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 (46) hide show
  1. {suite_py-1.44.0 → suite_py-1.46.0}/PKG-INFO +2 -2
  2. {suite_py-1.44.0 → suite_py-1.46.0}/pyproject.toml +2 -2
  3. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/__version__.py +1 -1
  4. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/cli.py +22 -0
  5. suite_py-1.46.0/suite_py/commands/estimate_cone.py +91 -0
  6. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/youtrack_handler.py +76 -3
  7. {suite_py-1.44.0 → suite_py-1.46.0}/LICENSE-APACHE +0 -0
  8. {suite_py-1.44.0 → suite_py-1.46.0}/LICENSE-MIT +0 -0
  9. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/__init__.py +0 -0
  10. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/__init__.py +0 -0
  11. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/ask_review.py +0 -0
  12. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/bump.py +0 -0
  13. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/check.py +0 -0
  14. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/common.py +0 -0
  15. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/context.py +0 -0
  16. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/create_branch.py +0 -0
  17. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/login.py +0 -0
  18. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/merge_pr.py +0 -0
  19. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/open_pr.py +0 -0
  20. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/project_lock.py +0 -0
  21. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/release.py +0 -0
  22. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/set_token.py +0 -0
  23. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/commands/status.py +0 -0
  24. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/__init__.py +0 -0
  25. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/config.py +0 -0
  26. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/__init__.py +0 -0
  27. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/aws_handler.py +0 -0
  28. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/captainhook_handler.py +0 -0
  29. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/changelog_handler.py +0 -0
  30. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/frequent_reviewers_handler.py +0 -0
  31. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/git_handler.py +0 -0
  32. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/github_handler.py +0 -0
  33. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/metrics_handler.py +0 -0
  34. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/okta_handler.py +0 -0
  35. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/pre_commit_handler.py +0 -0
  36. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/prompt_utils.py +0 -0
  37. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/handler/version_handler.py +0 -0
  38. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/logger.py +0 -0
  39. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/metrics.py +0 -0
  40. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/oauth.py +0 -0
  41. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/requests/__init__.py +0 -0
  42. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/requests/auth.py +0 -0
  43. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/requests/session.py +0 -0
  44. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/symbol.py +0 -0
  45. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/lib/tokens.py +0 -0
  46. {suite_py-1.44.0 → suite_py-1.46.0}/suite_py/templates/login.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: suite-py
3
- Version: 1.44.0
3
+ Version: 1.46.0
4
4
  Summary:
5
5
  Author: larrywax, EugenioLaghi, michelangelomo
6
6
  Author-email: devops@prima.it
@@ -28,7 +28,7 @@ Requires-Dist: pytest (>=7.0.0)
28
28
  Requires-Dist: python-dateutil (>=2.8.2)
29
29
  Requires-Dist: requests (>=2.26.0)
30
30
  Requires-Dist: requests-toolbelt (>=0.9.1)
31
- Requires-Dist: rich (==14.0.0)
31
+ Requires-Dist: rich (==14.1.0)
32
32
  Requires-Dist: semver (>=3.0.4,<4.0.0)
33
33
  Requires-Dist: termcolor (>=1.1.0)
34
34
  Requires-Dist: truststore (>=0.7,<0.11) ; 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.44.0"
5
+ version = "1.46.0"
6
6
 
7
7
  [tool.poetry.dependencies]
8
8
  Click = ">=7.0"
@@ -23,7 +23,7 @@ python = ">=3.8,<4.0"
23
23
  python-dateutil = ">=2.8.2"
24
24
  requests = ">=2.26.0"
25
25
  requests-toolbelt = ">=0.9.1"
26
- rich = "==14.0.0"
26
+ rich = "==14.1.0"
27
27
  semver = "^3.0.4"
28
28
  termcolor = ">=1.1.0"
29
29
  truststore = {version = ">=0.7,<0.11", python = ">=3.10"}
@@ -1,2 +1,2 @@
1
1
  # -*- encoding: utf-8 -*-
2
- __version__ = "1.44.0"
2
+ __version__ = "1.46.0"
@@ -265,6 +265,28 @@ def cli_unlock_project(obj, environment):
265
265
  obj.call(ProjectLock, env=environment, action="unlock").run()
266
266
 
267
267
 
268
+ @main.command(
269
+ "estimate-cone",
270
+ help="Point-in-time estimate of the time needed to complete all unresolved children of a card using the Cone of Uncertainty",
271
+ )
272
+ @click.option("--issue", prompt="Issue ID", type=str)
273
+ @click.option("--sprint-board", prompt="Sprint Board ID", type=str)
274
+ @click.option("--previous-sprints", required=False, type=int, default=6)
275
+ @click.pass_obj
276
+ @catch_exceptions
277
+ def estimate_cone(
278
+ obj: Context,
279
+ issue: Optional[str],
280
+ sprint_board: Optional[str],
281
+ previous_sprints: int,
282
+ ):
283
+ from suite_py.commands.estimate_cone import EstimateCone
284
+
285
+ obj.call(EstimateCone).run(
286
+ issue=issue, sprint_board=sprint_board, previous_sprints=previous_sprints
287
+ )
288
+
289
+
268
290
  @main.command("open-pr", help="Open a PR on GitHub")
269
291
  @click.pass_obj
270
292
  @catch_exceptions
@@ -0,0 +1,91 @@
1
+ # -*- encoding: utf-8 -*-
2
+ from suite_py.lib import metrics
3
+ from suite_py.lib.handler.youtrack_handler import YoutrackHandler
4
+
5
+
6
+ class EstimateCone:
7
+ def __init__(self, project, config, tokens):
8
+ self._project = project
9
+ self._config = config
10
+ self._youtrack = YoutrackHandler(config, tokens)
11
+
12
+ @metrics.command("estimate-cone")
13
+ def run(self, issue: str, sprint_board: str, previous_sprints: int):
14
+ remaining_story_points = self._sum_of_unresolved_descendant_story_points(issue)
15
+
16
+ current_sprint = self._youtrack.get_current_sprint(sprint_board)
17
+ sprints = self._youtrack.get_sprints(sprint_board)
18
+
19
+ current_sprint_index = next(
20
+ (
21
+ index
22
+ for index, sprint in enumerate(sprints)
23
+ if sprint["id"] == current_sprint["id"]
24
+ ),
25
+ None,
26
+ )
27
+
28
+ recent_sprints = sprints[
29
+ current_sprint_index - previous_sprints : current_sprint_index
30
+ ]
31
+
32
+ sprints_resolved_story_points = [
33
+ self._youtrack.get_sprint_resolved_story_points(sprint_board, sprint["id"])
34
+ for sprint in recent_sprints
35
+ ]
36
+
37
+ average_story_points_per_sprint = sum(sprints_resolved_story_points) / len(
38
+ sprints_resolved_story_points
39
+ )
40
+ max_story_points_per_sprint = (
41
+ max(sprints_resolved_story_points) or 1
42
+ ) # Avoid division by zero
43
+ min_story_points_per_sprint = (
44
+ min(sprints_resolved_story_points) or 1
45
+ ) # Avoid division by zero
46
+
47
+ average_estimate = remaining_story_points / average_story_points_per_sprint
48
+ best_estimate = remaining_story_points / max_story_points_per_sprint
49
+ worst_estimate = remaining_story_points / min_story_points_per_sprint
50
+
51
+ print(f"Remaining story points: {remaining_story_points}")
52
+
53
+ print(f"Max story points per sprint: {max_story_points_per_sprint}")
54
+ print(f"Average story points per sprint: {average_story_points_per_sprint:.2f}")
55
+ print(f"Min story points per sprint: {min_story_points_per_sprint}")
56
+
57
+ print(f"Average estimate: {average_estimate:.2f} sprints")
58
+ print(f"Best estimate: {best_estimate:.2f} sprints")
59
+ print(f"Worst estimate: {worst_estimate:.2f} sprints")
60
+
61
+ print(f"Sprints resolved story points: {sprints_resolved_story_points}")
62
+
63
+ def _sum_of_unresolved_descendant_story_points(self, _issue_readable_id):
64
+ descendants = self._youtrack.get_issue_descendants(_issue_readable_id)
65
+
66
+ descendants_digested = [
67
+ {
68
+ "id": descendant.get("idReadable"),
69
+ "summary": descendant.get("summary"),
70
+ "resolved": bool(descendant.get("resolved")),
71
+ "story_points": next(
72
+ (
73
+ field["value"]
74
+ for field in descendant["customFields"]
75
+ if field["name"] == "Story Points"
76
+ )
77
+ )
78
+ or 0,
79
+ }
80
+ for descendant in descendants
81
+ ]
82
+
83
+ for descendant in descendants_digested:
84
+ if not descendant["resolved"]:
85
+ print(descendant)
86
+
87
+ return sum(
88
+ descendant["story_points"]
89
+ for descendant in descendants_digested
90
+ if not descendant["resolved"]
91
+ )
@@ -23,6 +23,7 @@ WORKABLE_CARD_TYPES = [
23
23
  ]
24
24
 
25
25
 
26
+ # pylint: disable=too-many-public-methods
26
27
  class YoutrackHandler:
27
28
  def __init__(self, config, tokens):
28
29
  headers = {"Accept": "application/json", "Content-Type": "application/json"}
@@ -43,9 +44,15 @@ class YoutrackHandler:
43
44
 
44
45
  def get_issue(self, issue_id):
45
46
  logger.debug(f"YouTrack issue_id: {issue_id}")
46
- params = {
47
- "fields": "$type,id,idReadable,summary,parent(issues(id)),customFields(name,value(name))"
48
- }
47
+ _fields = [
48
+ "$type",
49
+ "id",
50
+ "idReadable",
51
+ "summary",
52
+ "parent(issues(id))",
53
+ "customFields(name,value(name))",
54
+ ]
55
+ params = {"fields": ",".join(_fields)}
49
56
  issue = self._client.get(f"issues/{issue_id}", params=params).json()
50
57
  issue["Type"] = self._get_issue_type_name(issue)
51
58
  logger.debug(f"YouTrack Issue json {issue}")
@@ -214,3 +221,69 @@ class YoutrackHandler:
214
221
  parent = issue["parent"]["issues"][0]
215
222
  # recursively get parent issue's type
216
223
  return self.get_issue(parent["id"])["Type"]
224
+
225
+ def get_issue_descendants(self, issue_id):
226
+ _fields = [
227
+ "resolved",
228
+ "subtasks(issues(id,idReadable,resolved,summary,customFields(name,value),subtasks(issues(id))))",
229
+ ]
230
+ params = {"fields": ",".join(_fields)}
231
+ issue = self._client.get(f"issues/{issue_id}", params=params).json()
232
+
233
+ descendants = []
234
+ for child in issue.get("subtasks", {}).get("issues", []):
235
+ descendants.append(child)
236
+ if child.get("subtasks", {}).get("issues"):
237
+ descendants.extend(self.get_issue_descendants(child["id"]))
238
+ return descendants
239
+
240
+ def get_current_sprint(self, agile_board_id):
241
+ params = {
242
+ "fields": "id",
243
+ }
244
+ return self._client.get(
245
+ f"agiles/{agile_board_id}/sprints/current", params=params
246
+ ).json()
247
+
248
+ def get_sprints(self, agile_board_id):
249
+ params = {
250
+ "fields": "id",
251
+ }
252
+ return self._client.get(
253
+ f"agiles/{agile_board_id}/sprints", params=params
254
+ ).json()
255
+
256
+ def get_sprint_resolved_story_points(self, agile_board_id, sprint_id):
257
+ """Get the sum of the story point estimates of all resolved issues of a sprint."""
258
+ params = {
259
+ "fields": "id,name,goal,issues(id,idReadable,summary,resolved,customFields(name,value))",
260
+ }
261
+ sprint = self._client.get(
262
+ f"agiles/{agile_board_id}/sprints/{sprint_id}", params=params
263
+ ).json()
264
+
265
+ resolved_issues = [issue for issue in sprint["issues"] if issue["resolved"]]
266
+
267
+ total_story_points = 0
268
+ for issue in resolved_issues:
269
+ # Find the Story Points custom field
270
+ story_points_field = next(
271
+ (
272
+ field
273
+ for field in issue.get("customFields", [])
274
+ if field["name"] == "Story Points"
275
+ ),
276
+ None,
277
+ )
278
+ if story_points_field and story_points_field.get("value"):
279
+ try:
280
+ story_points = int(story_points_field["value"])
281
+ total_story_points += story_points
282
+ except (ValueError, TypeError):
283
+ # Skip issues with invalid story points values
284
+ logger.debug(
285
+ f"Invalid story points value for issue {issue.get('idReadable', 'unknown')}: {story_points_field.get('value')}"
286
+ )
287
+ continue
288
+
289
+ return total_story_points
File without changes
File without changes