pipekit-sdk 2.0.1__tar.gz → 2.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pipekit-sdk
3
- Version: 2.0.1
3
+ Version: 2.1.0
4
4
  Summary: Pipekit Python SDK
5
5
  Home-page: https://pipekit.io
6
6
  License: BSD-3-Clause
@@ -16,8 +16,10 @@ Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
19
20
  Requires-Dist: hera (>=5.5.2)
20
21
  Requires-Dist: pydantic (<3)
22
+ Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
21
23
  Requires-Dist: responses (>=0.25.3,<0.26.0)
22
24
  Project-URL: Documentation, https://docs.pipekit.io/
23
25
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pipekit-sdk"
3
- version = "2.0.1"
3
+ version = "2.1.0"
4
4
  description = "Pipekit Python SDK"
5
5
  authors = ["Pipekit <ci@pipekit.io>"]
6
6
  license = "BSD-3-Clause"
@@ -27,6 +27,7 @@ python = ">=3.8,<4"
27
27
  hera = ">=5.5.2"
28
28
  pydantic = "<3"
29
29
  responses = "^0.25.3"
30
+ pyjwt = "^2.9.0"
30
31
 
31
32
  [tool.poetry.group.dev.dependencies]
32
33
  ruff = "*"
@@ -2,19 +2,71 @@
2
2
 
3
3
  import json
4
4
  import os
5
- from typing import Any, Generator, Optional, cast
5
+ from datetime import datetime
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Any, Generator, List, Optional, cast
6
9
 
10
+ import jwt
7
11
  import requests
8
12
  from hera.workflows import CronWorkflow, Workflow
9
13
  from hera.workflows.models import WorkflowCreateRequest
14
+ from pydantic import BaseModel, ValidationError
10
15
 
11
16
  from pipekit_sdk._helpers import is_valid_uuid
12
17
  from pipekit_sdk.models.model import Cluster, Logs, Pipe, PipeRun
13
18
 
14
19
 
20
+ class _LoginResponse(BaseModel):
21
+ """LoginResponse is a model for Login responses."""
22
+
23
+ token_type: str
24
+ access_token: str
25
+ expires_in: int
26
+ refresh_token: str
27
+
28
+
29
+ class _AccessToken(BaseModel):
30
+ """AccessToken is a model for decoded access tokens."""
31
+
32
+ identityUUID: str
33
+ ClusterUUID: str
34
+ OrgUUID: str
35
+ isActive: bool
36
+ products: List[str]
37
+ iss: str
38
+ sub: str
39
+ exp: int
40
+
41
+
42
+ class _ValidationResult(Enum):
43
+ OK = 0
44
+ INVALID_TOKEN = 1
45
+ EXPIRED_TOKEN = 2
46
+
47
+
15
48
  class PipekitService:
16
49
  """PipekitService is a wrapper around the Pipekit API. It provides a simple interface to interact with Pipekit."""
17
50
 
51
+ @staticmethod
52
+ def __from_home_dir() -> Optional[_LoginResponse]:
53
+ home = Path.home()
54
+ pipekit_token_file_path = os.path.join(home, ".pipekit", "token")
55
+ try:
56
+ with open(pipekit_token_file_path, "r") as f:
57
+ raw_data = f.read()
58
+ return _LoginResponse.model_validate_json(raw_data)
59
+ except ValidationError:
60
+ return None
61
+
62
+ @staticmethod
63
+ def __write_home_dir(login_resp: _LoginResponse):
64
+ home = Path.home()
65
+ pipekit_token_file_path = os.path.join(home, ".pipekit", "token")
66
+ with open(pipekit_token_file_path, "w") as f:
67
+ payload = login_resp.model_dump_json(indent=2)
68
+ f.write(payload)
69
+
18
70
  def __init__(
19
71
  self,
20
72
  username: str = "",
@@ -38,8 +90,60 @@ class PipekitService:
38
90
  self.access_token = token
39
91
  return
40
92
 
93
+ maybe_resp = self.__from_home_dir()
94
+ self.token_data = maybe_resp
95
+
96
+ if self.token_data is not None:
97
+ self.__token_login_flow()
98
+ return
99
+
41
100
  self.__login()
42
101
 
102
+ def __update_token(self) -> None:
103
+ login_url = f"{self.id_url}/api/id/v1/oauth/token"
104
+ assert self.token_data is not None, "Token data was None, this implies a bug"
105
+ login_request = {"grant_type": "refresh_token", "refresh_token": self.token_data.refresh_token}
106
+ login_response = requests.post(
107
+ login_url,
108
+ data=json.dumps(login_request),
109
+ headers={"Content-Type": "application/json"},
110
+ timeout=10,
111
+ )
112
+ if login_response.status_code // 100 != 2:
113
+ raise Exception(
114
+ "Couldn't refresh access token, obtained non 2xx status code, try logging in again via the cli"
115
+ )
116
+
117
+ login_response_model = _LoginResponse.model_validate(login_response.json())
118
+ self.__write_home_dir(login_response_model)
119
+ self.access_token = login_response_model.access_token
120
+
121
+ def __validate_token(self) -> _ValidationResult:
122
+ assert self.token_data is not None
123
+ try:
124
+ decoded_payload = jwt.decode(self.token_data.access_token, options={"verify_signature": False})
125
+ acc_tk = _AccessToken.model_validate(decoded_payload)
126
+ exp_time = datetime.fromtimestamp(acc_tk.exp)
127
+ now_time = datetime.now()
128
+
129
+ if exp_time <= now_time:
130
+ return _ValidationResult.EXPIRED_TOKEN
131
+ except jwt.PyJWTError:
132
+ return _ValidationResult.INVALID_TOKEN
133
+ return _ValidationResult.OK
134
+
135
+ def __token_login_flow(self) -> None:
136
+ assert self.token_data is not None, "token was None when it was expected to be valid"
137
+ res = self.__validate_token()
138
+ if res == _ValidationResult.EXPIRED_TOKEN:
139
+ self.__update_token()
140
+ elif res == _ValidationResult.INVALID_TOKEN:
141
+ raise RuntimeError(
142
+ "Token was invalid, if the `.pipekit/token` file is corrupted, please delete the file and login again"
143
+ )
144
+ else:
145
+ self.access_token = self.token_data.access_token
146
+
43
147
  def __login(self) -> None:
44
148
  login_request = {
45
149
  "username": self.username,
@@ -486,3 +590,7 @@ class PipekitService:
486
590
 
487
591
  for log in self.get_logs(run_uuid, pod_name=pod_name, container_name=container_name):
488
592
  self.__print_log_message(log)
593
+
594
+
595
+ if __name__ == "__main__":
596
+ p = PipekitService()
File without changes
File without changes