fluidattacks_gitlab_sdk 2.0.1__tar.gz → 2.2.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 (89) hide show
  1. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/PKG-INFO +2 -2
  2. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/__init__.py +1 -1
  3. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_logger.py +6 -13
  4. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/ids.py +10 -0
  5. fluidattacks_gitlab_sdk-2.2.0/fluidattacks_gitlab_sdk/jobs/__init__.py +3 -0
  6. fluidattacks_gitlab_sdk-2.2.0/fluidattacks_gitlab_sdk/jobs/_client.py +72 -0
  7. fluidattacks_gitlab_sdk-2.2.0/fluidattacks_gitlab_sdk/jobs/_decode.py +157 -0
  8. fluidattacks_gitlab_sdk-2.2.0/fluidattacks_gitlab_sdk/jobs/core.py +93 -0
  9. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/pyproject.toml +1 -1
  10. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/arch/arch.py +2 -1
  11. fluidattacks_gitlab_sdk-2.2.0/tests/jobs/data.json +87 -0
  12. fluidattacks_gitlab_sdk-2.2.0/tests/jobs/test_decode_job.py +64 -0
  13. fluidattacks_gitlab_sdk-2.2.0/tests_fx/jobs/client_test.py +50 -0
  14. fluidattacks_gitlab_sdk-2.2.0/tests_fx/mr_approvals/__init__.py +0 -0
  15. fluidattacks_gitlab_sdk-2.2.0/tests_fx/py.typed +0 -0
  16. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/uv.lock +102 -136
  17. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/.envrc +0 -0
  18. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/build/default.nix +0 -0
  19. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/build/filter.nix +0 -0
  20. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/flake.lock +0 -0
  21. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/flake.nix +0 -0
  22. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_decoders.py +0 -0
  23. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_gql_client/__init__.py +0 -0
  24. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_gql_client/_client.py +0 -0
  25. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_gql_client/_error.py +0 -0
  26. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_gql_client/_handlers.py +0 -0
  27. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_handlers.py +0 -0
  28. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_http_client/__init__.py +0 -0
  29. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_http_client/_client_1.py +0 -0
  30. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/_http_client/_core.py +0 -0
  31. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/issues/__init__.py +0 -0
  32. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/issues/_client/__init__.py +0 -0
  33. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/issues/_client/_decode.py +0 -0
  34. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/issues/_client/_get_issue.py +0 -0
  35. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/issues/_client/_most_recent.py +0 -0
  36. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/issues/_client/_updated_by.py +0 -0
  37. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/issues/core.py +0 -0
  38. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/members/__init__.py +0 -0
  39. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/members/_client.py +0 -0
  40. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/members/_decode.py +0 -0
  41. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/members/core.py +0 -0
  42. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/merge_requests/__init__.py +0 -0
  43. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/merge_requests/_client.py +0 -0
  44. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/merge_requests/_decode.py +0 -0
  45. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/merge_requests/core.py +0 -0
  46. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/milestones/__init__.py +0 -0
  47. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/milestones/_client.py +0 -0
  48. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/milestones/_decode.py +0 -0
  49. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/milestones/core.py +0 -0
  50. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/mr_approvals/__init__.py +0 -0
  51. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/mr_approvals/_client.py +0 -0
  52. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/mr_approvals/_decode.py +0 -0
  53. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/mr_approvals/core.py +0 -0
  54. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/py.typed +0 -0
  55. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/users/__init__.py +0 -0
  56. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/users/core.py +0 -0
  57. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/fluidattacks_gitlab_sdk/users/decode.py +0 -0
  58. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/mypy.ini +0 -0
  59. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/ruff.toml +0 -0
  60. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/__init__.py +0 -0
  61. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/arch/__init__.py +0 -0
  62. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/arch/test_arch.py +0 -0
  63. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/issues/__init__.py +0 -0
  64. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/issues/data.json +0 -0
  65. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/issues/test_decode.py +0 -0
  66. {fluidattacks_gitlab_sdk-2.0.1/tests/members → fluidattacks_gitlab_sdk-2.2.0/tests/jobs}/__init__.py +0 -0
  67. {fluidattacks_gitlab_sdk-2.0.1/tests/merge_request → fluidattacks_gitlab_sdk-2.2.0/tests/members}/__init__.py +0 -0
  68. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/members/data.json +0 -0
  69. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/members/test_decode_members.py +0 -0
  70. {fluidattacks_gitlab_sdk-2.0.1/tests/mr_approvals → fluidattacks_gitlab_sdk-2.2.0/tests/merge_request}/__init__.py +0 -0
  71. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/merge_request/data.json +0 -0
  72. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/merge_request/data_mr_updates.json +0 -0
  73. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/merge_request/test_decode.py +0 -0
  74. {fluidattacks_gitlab_sdk-2.0.1/tests_fx → fluidattacks_gitlab_sdk-2.2.0/tests/mr_approvals}/__init__.py +0 -0
  75. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/mr_approvals/data.json +0 -0
  76. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/mr_approvals/decoder_test.py +0 -0
  77. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests/py.typed +0 -0
  78. {fluidattacks_gitlab_sdk-2.0.1/tests_fx/issues → fluidattacks_gitlab_sdk-2.2.0/tests_fx}/__init__.py +0 -0
  79. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests_fx/_utils.py +0 -0
  80. {fluidattacks_gitlab_sdk-2.0.1/tests_fx/members → fluidattacks_gitlab_sdk-2.2.0/tests_fx/issues}/__init__.py +0 -0
  81. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests_fx/issues/test_issue_client.py +0 -0
  82. {fluidattacks_gitlab_sdk-2.0.1/tests_fx/milestones → fluidattacks_gitlab_sdk-2.2.0/tests_fx/jobs}/__init__.py +0 -0
  83. {fluidattacks_gitlab_sdk-2.0.1/tests_fx/mr_approvals → fluidattacks_gitlab_sdk-2.2.0/tests_fx/members}/__init__.py +0 -0
  84. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests_fx/members/test_client_members.py +0 -0
  85. /fluidattacks_gitlab_sdk-2.0.1/tests_fx/py.typed → /fluidattacks_gitlab_sdk-2.2.0/tests_fx/milestones/__init__.py +0 -0
  86. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests_fx/milestones/client_test.py +0 -0
  87. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests_fx/mr_approvals/client_test.py +0 -0
  88. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests_fx/test_gql_client.py +0 -0
  89. {fluidattacks_gitlab_sdk-2.0.1 → fluidattacks_gitlab_sdk-2.2.0}/tests_fx/test_mr.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluidattacks_gitlab_sdk
3
- Version: 2.0.1
3
+ Version: 2.2.0
4
4
  Summary: gitlab SDK
5
5
  Author-email: Product Team <development@fluidattacks.com>
6
6
  Requires-Python: >=3.11
@@ -10,4 +10,4 @@ Requires-Dist: fa-purity >=2.5.2, <3.0.0
10
10
  Requires-Dist: pure-requests >=3.0.0, <4.0.0
11
11
  Requires-Dist: python-dateutil >=2.8.0, <3.0.0
12
12
  Requires-Dist: fluidattacks-etl-utils >=2.0.0, <3.0.0
13
- Requires-Dist: fluidattacks-utils-logger >=2.0.0, <3.0.0
13
+ Requires-Dist: fluidattacks-utils-logger >=3.0.0, <4.0.0
@@ -11,7 +11,7 @@ from ._logger import (
11
11
  set_logger,
12
12
  )
13
13
 
14
- __version__ = "2.0.1"
14
+ __version__ = "2.2.0"
15
15
 
16
16
  Unsafe.compute(set_logger(__name__, __version__))
17
17
  LOG = logging.getLogger(__name__)
@@ -6,7 +6,6 @@ from fluidattacks_utils_logger import (
6
6
  )
7
7
  from fluidattacks_utils_logger.env import (
8
8
  current_app_env,
9
- notifier_key,
10
9
  observes_debug,
11
10
  )
12
11
  from fluidattacks_utils_logger.handlers import (
@@ -15,20 +14,14 @@ from fluidattacks_utils_logger.handlers import (
15
14
 
16
15
 
17
16
  def set_logger(root_name: str, version: str) -> Cmd[None]:
18
- n_key = notifier_key()
19
17
  app_env = current_app_env()
20
18
  debug = observes_debug()
21
- conf = n_key.bind(
22
- lambda key: app_env.map(
23
- lambda env: LoggingConf(
24
- "sdk",
25
- version,
26
- "./observes/sdk/fluidattacks_gitlab_sdk",
27
- False,
28
- key,
29
- env,
30
- "observes",
31
- ),
19
+ conf = app_env.map(
20
+ lambda env: LoggingConf(
21
+ app_name="observes",
22
+ app_type="sdk",
23
+ app_version=version,
24
+ release_stage=env,
32
25
  ),
33
26
  )
34
27
  return debug.bind(lambda d: conf.bind(lambda c: set_main_log(root_name, c, d)))
@@ -133,3 +133,13 @@ class IssueFullInternalId:
133
133
  class IssueFullId:
134
134
  global_id: IssueGlobalId
135
135
  internal_id: IssueFullInternalId
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class JobId:
140
+ job_id: Natural
141
+
142
+
143
+ @dataclass(frozen=True)
144
+ class RunnerId:
145
+ runner_id: Natural
@@ -0,0 +1,3 @@
1
+ from ._client import JobClientFactory
2
+
3
+ __all__ = ["JobClientFactory"]
@@ -0,0 +1,72 @@
1
+ from __future__ import (
2
+ annotations,
3
+ )
4
+
5
+ import inspect
6
+ import logging
7
+ from dataclasses import dataclass
8
+
9
+ from fa_purity import Cmd, Coproduct, FrozenDict, Result, cast_exception
10
+ from fluidattacks_etl_utils.bug import Bug
11
+ from fluidattacks_etl_utils.decode import int_to_str
12
+ from fluidattacks_etl_utils.smash import right_map
13
+
14
+ from fluidattacks_gitlab_sdk._decoders import assert_single
15
+ from fluidattacks_gitlab_sdk._handlers import NotFound, handle_not_found
16
+ from fluidattacks_gitlab_sdk._http_client import (
17
+ ClientFactory,
18
+ Credentials,
19
+ HttpJsonClient,
20
+ RelativeEndpoint,
21
+ )
22
+ from fluidattacks_gitlab_sdk.ids import JobId, ProjectId
23
+
24
+ from ._decode import decode_job_obj
25
+ from .core import JobClient, JobObj
26
+
27
+ LOG = logging.getLogger(__name__)
28
+
29
+
30
+ def get_job(
31
+ client: HttpJsonClient,
32
+ project: ProjectId,
33
+ job_id: JobId,
34
+ ) -> Cmd[Result[JobObj, Coproduct[NotFound, Exception]]]:
35
+ endpoint = RelativeEndpoint.new(
36
+ "projects",
37
+ int_to_str(project.project_id.value),
38
+ "jobs",
39
+ int_to_str(job_id.job_id.value),
40
+ )
41
+ msg = Cmd.wrap_impure(lambda: LOG.info("[API] get_job(%s, %s)", project, job_id))
42
+ return msg + client.get(endpoint, FrozenDict({})).map(
43
+ lambda r: r.alt(
44
+ lambda e: e.map(handle_not_found, lambda e: Coproduct.inr(cast_exception(e))),
45
+ )
46
+ .bind(lambda v: assert_single(v).alt(Coproduct.inr))
47
+ .bind(lambda v: decode_job_obj(v).alt(Coproduct.inr))
48
+ .alt(
49
+ lambda c: right_map(
50
+ c,
51
+ lambda e: cast_exception(
52
+ Bug.new(
53
+ "_get_job",
54
+ inspect.currentframe(),
55
+ e,
56
+ (),
57
+ ),
58
+ ),
59
+ ),
60
+ ),
61
+ )
62
+
63
+
64
+ def _from_client(client: HttpJsonClient) -> JobClient:
65
+ return JobClient(lambda p, j: get_job(client, p, j))
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class JobClientFactory:
70
+ @staticmethod
71
+ def new(creds: Credentials) -> JobClient:
72
+ return _from_client(ClientFactory.new(creds))
@@ -0,0 +1,157 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from decimal import Decimal
4
+
5
+ from dateutil.parser import (
6
+ isoparse,
7
+ )
8
+ from fa_purity import (
9
+ FrozenDict,
10
+ FrozenList,
11
+ Maybe,
12
+ Result,
13
+ ResultE,
14
+ ResultSmash,
15
+ )
16
+ from fa_purity.json import JsonObj, JsonPrimitiveUnfolder, JsonUnfolder, JsonValue, Unfolder
17
+ from fluidattacks_etl_utils import smash
18
+ from fluidattacks_etl_utils.decode import DecodeUtils
19
+ from fluidattacks_etl_utils.natural import Natural, NaturalOperations
20
+
21
+ from fluidattacks_gitlab_sdk.ids import JobId, RunnerId, UserId
22
+
23
+ from .core import Commit, CommitHash, Job, JobConf, JobDates, JobObj, JobResultStatus
24
+
25
+ LOG = logging.getLogger(__name__)
26
+
27
+
28
+ def _decoder_require_list(item: JsonValue) -> ResultE[FrozenList[str]]:
29
+ return Unfolder.to_list_of(
30
+ item,
31
+ lambda v: Unfolder.to_primitive(v).bind(
32
+ lambda p: JsonPrimitiveUnfolder.to_str(p),
33
+ ),
34
+ )
35
+
36
+
37
+ def get_float(number: JsonValue) -> ResultE[float]:
38
+ return Unfolder.to_primitive(number).bind(lambda v: JsonPrimitiveUnfolder.to_float(v))
39
+
40
+
41
+ def str_to_datetime(raw: str) -> ResultE[datetime]:
42
+ try:
43
+ return Result.success(isoparse(raw))
44
+ except ValueError as err:
45
+ return Result.failure(Exception(err))
46
+
47
+
48
+ def decode_str(value: JsonValue) -> ResultE[str]:
49
+ return Unfolder.to_primitive(value).bind(lambda p: JsonPrimitiveUnfolder.to_str(p))
50
+
51
+
52
+ def decode_datetime(value: JsonValue) -> ResultE[datetime]:
53
+ return decode_str(value).bind(str_to_datetime)
54
+
55
+
56
+ def decode_job_id(raw: JsonObj) -> ResultE[JobId]:
57
+ return JsonUnfolder.require(raw, "id", DecodeUtils.to_int).bind(Natural.from_int).map(JobId)
58
+
59
+
60
+ def decode_user_id(raw: JsonObj) -> ResultE[Maybe[UserId]]:
61
+ def decode_maybe_id(raw: JsonObj) -> ResultE[Maybe[UserId]]:
62
+ return JsonUnfolder.optional(
63
+ raw,
64
+ "id",
65
+ lambda v: Unfolder.to_primitive(v)
66
+ .bind(JsonPrimitiveUnfolder.to_int)
67
+ .map(lambda j: UserId(NaturalOperations.absolute(j))),
68
+ )
69
+
70
+ maybe_user_obj = JsonUnfolder.optional(raw, "user", lambda v: Unfolder.to_json(v))
71
+ return maybe_user_obj.bind(lambda v: decode_maybe_id(v.value_or(FrozenDict({}))))
72
+
73
+
74
+ def decode_runner_id(raw: JsonObj) -> ResultE[Maybe[RunnerId]]:
75
+ def decode_maybe_id(raw: JsonObj) -> ResultE[Maybe[RunnerId]]:
76
+ return JsonUnfolder.optional(
77
+ raw,
78
+ "id",
79
+ lambda v: Unfolder.to_primitive(v)
80
+ .bind(JsonPrimitiveUnfolder.to_int)
81
+ .map(lambda j: RunnerId(NaturalOperations.absolute(j))),
82
+ )
83
+
84
+ maybe_runner_obj = JsonUnfolder.optional(raw, "runner", lambda v: Unfolder.to_json(v))
85
+ return maybe_runner_obj.bind(lambda v: decode_maybe_id(v.value_or(FrozenDict({}))))
86
+
87
+
88
+ def decode_commit_properties(raw: JsonObj) -> ResultE[Commit]:
89
+ sha_commit = JsonUnfolder.optional(raw, "id", DecodeUtils.to_opt_str).map(
90
+ lambda v: v.bind(lambda j: j.map(lambda p: CommitHash(p))),
91
+ )
92
+ title = JsonUnfolder.optional(raw, "title", DecodeUtils.to_opt_str).map(
93
+ lambda v: v.bind(lambda j: j),
94
+ )
95
+ return ResultSmash.smash_result_2(sha_commit, title).map(lambda v: Commit(*v))
96
+
97
+
98
+ def decode_commit(raw: JsonObj) -> ResultE[Commit]:
99
+ maybe_commit_obj = JsonUnfolder.optional(raw, "commit", lambda v: Unfolder.to_json(v))
100
+ return maybe_commit_obj.bind(lambda v: decode_commit_properties(v.value_or(FrozenDict({}))))
101
+
102
+
103
+ def decode_job_dates(raw: JsonObj) -> ResultE[JobDates]:
104
+ created_at = JsonUnfolder.require(raw, "created_at", DecodeUtils.to_date_time)
105
+ started_at = JsonUnfolder.optional(raw, "started_at", DecodeUtils.to_opt_date_time).map(
106
+ lambda m: m.bind(lambda x: x),
107
+ )
108
+ finished_at = JsonUnfolder.optional(raw, "finished_at", DecodeUtils.to_opt_date_time).map(
109
+ lambda m: m.bind(lambda x: x),
110
+ )
111
+ return ResultSmash.smash_result_3(created_at, started_at, finished_at).map(
112
+ lambda t: JobDates(*t),
113
+ )
114
+
115
+
116
+ def decode_job_conf(raw: JsonObj) -> ResultE[JobConf]:
117
+ return smash.smash_result_4(
118
+ JsonUnfolder.require(raw, "allow_failure", DecodeUtils.to_bool),
119
+ JsonUnfolder.require(raw, "tag_list", _decoder_require_list),
120
+ JsonUnfolder.require(raw, "ref", DecodeUtils.to_str),
121
+ JsonUnfolder.require(raw, "stage", DecodeUtils.to_str),
122
+ ).map(lambda v: JobConf(*v))
123
+
124
+
125
+ def decode_job_result(raw: JsonObj) -> ResultE[JobResultStatus]:
126
+ return smash.smash_result_4(
127
+ JsonUnfolder.require(raw, "status", DecodeUtils.to_str),
128
+ JsonUnfolder.optional(raw, "failure_reason", DecodeUtils.to_opt_str).map(
129
+ lambda v: v.bind(lambda j: j),
130
+ ),
131
+ JsonUnfolder.optional(raw, "duration", lambda v: get_float(v).map(Decimal)),
132
+ JsonUnfolder.optional(raw, "queued_duration", lambda v: get_float(v).map(Decimal)),
133
+ ).map(lambda v: JobResultStatus(*v))
134
+
135
+
136
+ def decode_job(raw: JsonObj) -> ResultE[Job]:
137
+ first = smash.smash_result_5(
138
+ JsonUnfolder.require(raw, "name", DecodeUtils.to_str),
139
+ decode_user_id(raw),
140
+ decode_runner_id(raw),
141
+ JsonUnfolder.optional(raw, "coverage", get_float),
142
+ decode_commit(raw),
143
+ )
144
+ second = smash.smash_result_3(
145
+ decode_job_dates(raw),
146
+ decode_job_conf(raw),
147
+ decode_job_result(raw),
148
+ )
149
+
150
+ return smash.smash_result_2(first, second).map(lambda v: Job(*v[0], *v[1]))
151
+
152
+
153
+ def decode_job_obj(raw: JsonObj) -> ResultE[JobObj]:
154
+ id_job = JsonUnfolder.require(raw, "id", DecodeUtils.to_int).map(
155
+ lambda v: JobId(NaturalOperations.absolute(v)),
156
+ )
157
+ return smash.smash_result_2(id_job, decode_job(raw)).map(lambda v: JobObj(*v))
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from decimal import Decimal
6
+ from enum import Enum
7
+
8
+ from fa_purity import Cmd, Coproduct, FrozenList, Maybe, Result, ResultE
9
+ from fa_purity.date_time import DatetimeUTC
10
+
11
+ from fluidattacks_gitlab_sdk._handlers import NotFound
12
+ from fluidattacks_gitlab_sdk.ids import JobId, ProjectId, RunnerId, UserId
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class CommitHash:
17
+ hash_str: str
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Commit:
22
+ sha_commit: Maybe[CommitHash]
23
+ title_commit: Maybe[str]
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class JobDates:
28
+ created_at: DatetimeUTC
29
+ started_at: Maybe[DatetimeUTC]
30
+ finished_at: Maybe[DatetimeUTC]
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class JobConf:
35
+ allow_failure: bool
36
+ tag_list: FrozenList[str]
37
+ ref_branch: str
38
+ stage: str
39
+
40
+
41
+ class JobStatus(Enum):
42
+ CREATED = "created"
43
+ PENDING = "pending"
44
+ RUNNING = "running"
45
+ FAILED = "failed"
46
+ SUCCESS = "success"
47
+ CANCELED = "canceled"
48
+ SKIPPED = "skipped"
49
+ WAITING_FOR_RESOURCE = "waiting_for_resource"
50
+ MANUAL = "manual"
51
+
52
+ @staticmethod
53
+ def from_raw(raw: str) -> ResultE[JobStatus]:
54
+ try:
55
+ return Result.success(JobStatus(raw))
56
+ except ValueError as err:
57
+ return Result.failure(Exception(err))
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class JobResultStatus:
62
+ status: str
63
+ failure_reason: Maybe[str]
64
+ duration: Maybe[Decimal]
65
+ queued_duration: Maybe[Decimal]
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class Job:
70
+ name: str
71
+ user_id: Maybe[UserId]
72
+ runner_id: Maybe[RunnerId]
73
+ coverage: Maybe[float]
74
+ commit: Commit
75
+ dates: JobDates
76
+ conf: JobConf
77
+ result: JobResultStatus
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class JobObj:
82
+ job_id: JobId
83
+ obj: Job
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class JobClient:
88
+ get_job: Callable[[ProjectId, JobId], Cmd[Result[JobObj, Coproduct[NotFound, Exception]]]]
89
+
90
+
91
+ __all__ = [
92
+ "JobId",
93
+ ]
@@ -9,7 +9,7 @@ dependencies = [
9
9
  "pure-requests >=3.0.0, <4.0.0",
10
10
  "python-dateutil >=2.8.0, <3.0.0",
11
11
  "fluidattacks-etl-utils >=2.0.0, <3.0.0",
12
- "fluidattacks-utils-logger >=2.0.0, <3.0.0",
12
+ "fluidattacks-utils-logger >=3.0.0, <4.0.0",
13
13
  ]
14
14
  description = "gitlab SDK"
15
15
  dynamic = ["version"]
@@ -26,7 +26,7 @@ def _module(path: str) -> FullPathModule | NoReturn:
26
26
 
27
27
  _dag: Dict[str, tuple[tuple[str, ...] | str, ...]] = {
28
28
  "fluidattacks_gitlab_sdk": (
29
- ("merge_requests", "members", "issues", "milestones", "mr_approvals"),
29
+ ("merge_requests", "members", "issues", "milestones", "mr_approvals", "jobs"),
30
30
  "users",
31
31
  "_handlers",
32
32
  ("_http_client", "_gql_client", "_decoders"),
@@ -74,6 +74,7 @@ _dag: Dict[str, tuple[tuple[str, ...] | str, ...]] = {
74
74
  "_decode",
75
75
  "core",
76
76
  ),
77
+ "fluidattacks_gitlab_sdk.jobs": ("_client", "_decode", "core"),
77
78
  }
78
79
 
79
80
 
@@ -0,0 +1,87 @@
1
+ {
2
+ "id": 10891419293,
3
+ "status": "success",
4
+ "stage": "test",
5
+ "name": "integrates-back-test",
6
+ "ref": "developeratfluid",
7
+ "tag": false,
8
+ "coverage": null,
9
+ "allow_failure": false,
10
+ "created_at": "2025-08-01T23:49:19.0Z",
11
+ "started_at": "2025-08-01T23:49:22.0Z",
12
+ "finished_at": "2025-08-02T00:09:05.0Z",
13
+ "erased_at": null,
14
+ "duration": 1183.281855,
15
+ "queued_duration": 3.151458,
16
+ "user": {
17
+ "id": 4312487,
18
+ "username": "slizcanoatfluid",
19
+ "public_email": "",
20
+ "name": "Sebastian Lizcano",
21
+ "state": "active",
22
+ "locked": false,
23
+ "avatar_url": "https://secure.gravatar.com/avatar/1c17a07b5fd3f49c7f4a7f8163dd80681e198880aefed63082b409dd9936fcc5?s=80&d=identicon",
24
+ "web_url": "https://gitlab.com/slizcanoatfluid",
25
+ "bio": "",
26
+ "location": "",
27
+ "linkedin": "",
28
+ "twitter": "",
29
+ "discord": "",
30
+ "website_url": "",
31
+ "github": "",
32
+ "job_title": "",
33
+ "pronouns": "",
34
+ "organization": "",
35
+ "bot": false,
36
+ "work_information": null,
37
+ "local_time": "1:10 AM"
38
+ },
39
+ "commit": {
40
+ "id":"82717b572300d92cd8dab2d7b9fff6f0090c69f7",
41
+ "title": "integrates/refac(back) test"
42
+ },
43
+ "pipeline": {
44
+ "id": 1962092437,
45
+ "iid": 643611,
46
+ "project_id": 20741933,
47
+ "sha": "82717b572300d92cd8dab2d7b9fff6f0090c69f8",
48
+ "ref": "slizcanoatfluid",
49
+ "status": "failed",
50
+ "source": "push",
51
+ "created_at": "2025-08-01T23:04:29.732Z",
52
+ "updated_at": "2025-08-02T00:24:33.128Z",
53
+ "web_url": "https://gitlab.com/fluidattacks/universe/-/pipelines/1962092437"
54
+ },
55
+ "web_url": "https://gitlab.com/fluidattacks/universe/-/jobs/10891419293",
56
+ "project": {
57
+ "ci_job_token_scope_enabled": false
58
+ },
59
+ "artifacts": [
60
+ {
61
+ "file_type": "trace",
62
+ "size": 358079,
63
+ "filename": "job.log",
64
+ "file_format": null
65
+ }
66
+ ],
67
+ "runner": {
68
+ "id": 46453645,
69
+ "description": null,
70
+ "ip_address": null,
71
+ "active": true,
72
+ "paused": false,
73
+ "is_shared": false,
74
+ "runner_type": "group_type",
75
+ "name": null,
76
+ "online": true,
77
+ "created_at": "2025-03-13T19:51:43.296Z",
78
+ "status": "online",
79
+ "job_execution_status": "active"
80
+ },
81
+ "runner_manager": null,
82
+ "artifacts_expire_at": "2025-08-09T00:08:49.417Z",
83
+ "archived": false,
84
+ "tag_list": [
85
+ "aarch64"
86
+ ]
87
+ }
@@ -0,0 +1,64 @@
1
+ from datetime import UTC, datetime
2
+ from decimal import Decimal
3
+ from pathlib import Path
4
+
5
+ from fa_purity import Maybe, Unsafe
6
+ from fa_purity.date_time import DatetimeUTC
7
+ from fa_purity.json import JsonValueFactory, Unfolder
8
+ from fluidattacks_etl_utils.natural import NaturalOperations
9
+
10
+ from fluidattacks_gitlab_sdk.ids import JobId, RunnerId, UserId
11
+ from fluidattacks_gitlab_sdk.jobs._decode import decode_job_obj
12
+ from fluidattacks_gitlab_sdk.jobs.core import (
13
+ Commit,
14
+ CommitHash,
15
+ Job,
16
+ JobConf,
17
+ JobDates,
18
+ JobObj,
19
+ JobResultStatus,
20
+ )
21
+
22
+
23
+ def _assert_utc(date_time: datetime) -> DatetimeUTC:
24
+ return DatetimeUTC.assert_utc(date_time).alt(Unsafe.raise_exception).to_union()
25
+
26
+
27
+ def test_decode() -> None:
28
+ architectures = ("aarch64",)
29
+ expected_job = JobObj(
30
+ JobId(NaturalOperations.absolute(10891419293)),
31
+ Job(
32
+ "integrates-back-test",
33
+ Maybe.some(UserId(NaturalOperations.absolute(4312487))),
34
+ Maybe.some(RunnerId(NaturalOperations.absolute(46453645))), # int
35
+ Maybe.empty(),
36
+ Commit(
37
+ Maybe.some(CommitHash("82717b572300d92cd8dab2d7b9fff6f0090c69f7")),
38
+ Maybe.some("integrates/refac(back) test"),
39
+ ),
40
+ JobDates(
41
+ _assert_utc(datetime(2025, 8, 1, 23, 49, 19, tzinfo=UTC)),
42
+ Maybe.some(_assert_utc(datetime(2025, 8, 1, 23, 49, 22, tzinfo=UTC))),
43
+ Maybe.some(_assert_utc(datetime(2025, 8, 2, 0, 9, 5, tzinfo=UTC))),
44
+ ),
45
+ JobConf(False, architectures, "developeratfluid", "test"),
46
+ JobResultStatus(
47
+ "success",
48
+ Maybe.empty(),
49
+ Maybe.some(Decimal(1183.281855)), # noqa: RUF032
50
+ Maybe.some(Decimal(3.151458)), # noqa: RUF032
51
+ ),
52
+ ),
53
+ )
54
+
55
+ raw_path = Path(__file__).parent / "data.json"
56
+ raw_data = (
57
+ JsonValueFactory.load(raw_path.open(encoding="utf-8"))
58
+ .bind(Unfolder.to_json)
59
+ .alt(Unsafe.raise_exception)
60
+ .to_union()
61
+ )
62
+
63
+ decode_obj = decode_job_obj(raw_data).alt(Unsafe.raise_exception).to_union()
64
+ assert decode_obj == expected_job
@@ -0,0 +1,50 @@
1
+ import pytest
2
+ from fa_purity import Cmd, Coproduct, Result, Unsafe
3
+ from fluidattacks_etl_utils.natural import NaturalOperations
4
+
5
+ from fluidattacks_gitlab_sdk._handlers import NotFound
6
+ from fluidattacks_gitlab_sdk.ids import JobId
7
+ from fluidattacks_gitlab_sdk.jobs import JobClientFactory
8
+ from fluidattacks_gitlab_sdk.jobs.core import JobObj
9
+ from tests_fx._utils import get_creds_from_env, get_project_from_env
10
+
11
+
12
+ def _check_data(result: Result[JobObj, Coproduct[NotFound, Exception]]) -> None:
13
+ assert result.to_coproduct().map(
14
+ lambda _: True,
15
+ lambda c: c.map(
16
+ lambda _: False,
17
+ lambda _: False,
18
+ ),
19
+ )
20
+
21
+
22
+ def _check_not_found(
23
+ result: Result[JobObj, Coproduct[NotFound, Exception]],
24
+ ) -> None:
25
+ assert result.to_coproduct().map(
26
+ lambda _: False,
27
+ lambda c: c.map(
28
+ lambda _: True,
29
+ lambda _: False,
30
+ ),
31
+ )
32
+
33
+
34
+ def test_job() -> None:
35
+ get_creds = get_creds_from_env().map(lambda r: r.alt(Unsafe.raise_exception).to_union())
36
+ get_project = get_project_from_env().map(lambda r: r.alt(Unsafe.raise_exception).to_union())
37
+ action: Cmd[None] = (
38
+ get_creds.map(JobClientFactory.new)
39
+ .bind(
40
+ lambda c: get_project.bind(
41
+ lambda project: c.get_job(
42
+ project,
43
+ JobId(NaturalOperations.absolute(12634660510)),
44
+ ),
45
+ ),
46
+ )
47
+ .map(_check_data)
48
+ )
49
+ with pytest.raises(SystemExit):
50
+ action.compute()
File without changes