allspice
A very simple API client for AllSpice Hub
Note that not the full Swagger-API is accessible. The whole implementation is focused on making access and working with Organizations, Teams, Repositories and Users as pain free as possible.
Forked from https://github.com/Langenfeld/py-gitea.
Usage
Docs
See the documentation site.
Examples
Check the examples directory for full, working example scripts that you can adapt or refer to for your own needs.
Quickstart
First get an allspice_client object wrapping access and authentication (via an api token) for your instance of AllSpice Hub.
from allspice import *
# By default, points to hub.allspice.io.
allspice_client = AllSpice(token_text=TOKEN)
# If you are self-hosting:
allspice_client = AllSpice(allspice_hub_url=URL, token_text=TOKEN)
Operations like requesting the AllSpice version or authentication user can be requested directly from the allspice_client object:
print("AllSpice Version: " + allspice_client.get_version())
print("API-Token belongs to user: " + allspice_client.get_user().username)
Adding entities like Users, Organizations, ... also is done via the allspice_client object.
user = allspice_client.create_user("Test Testson", "test@test.test", "password")
All operations on entities in allspice are then accomplished via the according wrapper objects for those entities.
Each of those objects has a .request method that creates an entity according to your allspice_client instance.
other_user = User.request(allspice_client, "OtherUserName")
print(other_user.username)
Note that the fields of the User, Organization,... classes are dynamically created at runtime, and thus not visible during divelopment. Refer to the AllSpice API documentation for the fields names.
Fields that can not be altered via allspice-api, are read only. After altering a field, the .commit method of the according object must be called to synchronize the changed fields with your allspice_client instance.
org = Organization.request(allspice_client, test_org)
org.description = "some new description"
org.location = "some new location"
org.commit()
An entity in allspice can be deleted by calling delete.
org.delete()
All entity objects do have methods to execute some of the requests possible though the AllSpice api:
org = Organization.request(allspice_client, ORGNAME)
teams = org.get_teams()
for team in teams:
repos = team.get_repos()
for repo in repos:
print(repo.name)
1""" 2.. include:: ../README.md 3 :start-line: 1 4 :end-before: Installation 5""" 6 7from .allspice import ( 8 AllSpice, 9) 10from .apiobject import ( 11 Branch, 12 Comment, 13 Commit, 14 Content, 15 DesignReview, 16 DesignReviewReview, 17 Issue, 18 Milestone, 19 Organization, 20 Release, 21 Repository, 22 Team, 23 User, 24) 25from .exceptions import ( 26 AlreadyExistsException, 27 APIError, 28 InternalServerException, 29 NotFoundException, 30 RenderException, 31) 32 33__version__ = "4.3.0" 34 35__all__ = [ 36 "APIError", 37 "AllSpice", 38 "AlreadyExistsException", 39 "Branch", 40 "Comment", 41 "Commit", 42 "Content", 43 "DesignReview", 44 "DesignReviewReview", 45 "InternalServerException", 46 "Issue", 47 "Milestone", 48 "NotFoundException", 49 "Organization", 50 "Release", 51 "RenderException", 52 "Repository", 53 "Team", 54 "User", 55]
7@dataclass(frozen=True) 8class APIError: 9 """The body of an AllSpice Hub error response""" 10 11 message: Optional[str] 12 url: Optional[str] 13 category: Optional[str] 14 15 @classmethod 16 def from_json(cls, text: str) -> Optional["APIError"]: 17 try: 18 data = json.loads(text) 19 except json.JSONDecodeError: 20 return None 21 22 if not isinstance(data, dict): 23 return None 24 25 message = data.get("message") or None 26 url = data.get("url") or None 27 category = data.get("category") or None 28 29 if message is None and url is None and category is None: 30 return None 31 32 return cls(message=message, url=url, category=category)
The body of an AllSpice Hub error response
15 @classmethod 16 def from_json(cls, text: str) -> Optional["APIError"]: 17 try: 18 data = json.loads(text) 19 except json.JSONDecodeError: 20 return None 21 22 if not isinstance(data, dict): 23 return None 24 25 message = data.get("message") or None 26 url = data.get("url") or None 27 category = data.get("category") or None 28 29 if message is None and url is None and category is None: 30 return None 31 32 return cls(message=message, url=url, category=category)
43class AllSpice: 44 """Object to establish a session with AllSpice Hub.""" 45 46 ADMIN_CREATE_USER = """/admin/users""" 47 GET_USERS_ADMIN = """/admin/users""" 48 ADMIN_REPO_CREATE = """/admin/users/%s/repos""" # <ownername> 49 ALLSPICE_HUB_VERSION = """/version""" 50 GET_USER = """/user""" 51 GET_REPOSITORY = """/repos/{owner}/{name}""" 52 CREATE_ORG = """/admin/users/%s/orgs""" # <username> 53 CREATE_TEAM = """/orgs/%s/teams""" # <orgname> 54 55 def __init__( 56 self, 57 allspice_hub_url="https://hub.allspice.io", 58 token_text=None, 59 auth=None, 60 verify=True, 61 log_level="INFO", 62 ratelimiting=(100, 60), 63 retry: Union[Retry, int, None] = DEFAULT_RETRY, 64 use_new_schdoc_renderer: Optional[bool] = None, 65 ): 66 """Initializing an instance of the AllSpice Hub Client 67 68 Args: 69 allspice_hub_url (str): The URL for the AllSpice Hub instance. 70 Defaults to `https://hub.allspice.io`. 71 72 token_text (str, None): The access token, by default None. 73 74 auth (tuple, None): The user credentials 75 `(username, password)`, by default None. 76 77 verify (bool): If True, allow insecure server connections 78 when using SSL. 79 80 log_level (str): The log level, by default `INFO`. 81 82 ratelimiting (tuple[int, int], None): `(max_calls, period)`, 83 If None, no rate limiting is applied. By default, 100 calls 84 per minute are allowed. 85 86 retry (Retry, int, None): Set a retry policy for requests using a 87 urllib3 Retry object, an integer for the number of retries, or None 88 for no retries. This happens before py-allspice's own rate limiter. 89 The default is to retry all methods (even mutating methods) 90 only on 429s and 502s up to 6 times with exponential backoff 91 and jitter. 92 93 NOTE: AllSpice Hub responds with a 503 and a Retry-After header 94 when a generated file is not ready yet, and urllib3 retries any 95 503 that carries Retry-After. So with retries enabled, fetching 96 generated files transparently waits for generation instead of 97 raising NotYetGeneratedException, until retries are exhausted. 98 99 use_new_schdoc_renderer (bool): Allows explicit override for using the new Altium schematic renderer. If set, 100 this will take precedence over the default behavior on the AllSpice Hub instance. 101 """ 102 103 self.logger = logging.getLogger(__name__) 104 handler = logging.StreamHandler(sys.stderr) 105 handler.setFormatter( 106 logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 107 ) 108 self.logger.addHandler(handler) 109 self.logger.setLevel(log_level) 110 self.headers = { 111 "Content-type": "application/json", 112 } 113 self.url = allspice_hub_url 114 115 if ratelimiting is None: 116 self.requests = requests.Session() 117 else: 118 (max_calls, period) = ratelimiting 119 self.requests = RateLimitedSession(max_calls=max_calls, period=period) 120 121 if retry is not None: 122 adapter = HTTPAdapter(max_retries=retry) 123 self.requests.mount("https://", adapter) 124 self.requests.mount("http://", adapter) 125 126 # Manage authentification 127 if not token_text and not auth: 128 raise ValueError("Please provide auth or token_text, but not both") 129 if token_text: 130 self.headers["Authorization"] = "token " + token_text 131 if auth: 132 self.logger.warning( 133 "Using basic auth is not recommended. Prefer using a token instead." 134 ) 135 self.requests.auth = auth 136 137 # Manage SSL certification verification 138 self.requests.verify = verify 139 if not verify: 140 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 141 142 self.use_new_schdoc_renderer = use_new_schdoc_renderer 143 144 def __get_url(self, endpoint): 145 url = self.url + "/api/v1" + endpoint 146 self.logger.debug("Url: %s" % url) 147 return url 148 149 def __get(self, endpoint: str, params: Mapping = frozendict()) -> requests.Response: 150 response = self.requests.get(self.__get_url(endpoint), headers=self.headers, params=params) 151 if response.status_code not in [200, 201]: 152 message = f"Received status code: {response.status_code} ({response.url})" 153 if response.status_code in [404]: 154 raise NotFoundException(message) 155 if response.status_code in [403]: 156 raise Exception( 157 f"Unauthorized: {response.url} - Check your permissions and try again! ({message})" 158 ) 159 if response.status_code in [409]: 160 raise ConflictException(message) 161 if response.status_code in [503]: 162 raise NotYetGeneratedException(message) 163 if response.status_code in [500]: 164 raise InternalServerException(message, APIError.from_json(response.text)) 165 raise Exception(message) 166 return response 167 168 @staticmethod 169 def parse_result(result) -> Dict: 170 """Parses the result-JSON to a dict.""" 171 if result.text and len(result.text) > 3: 172 return json.loads(result.text) 173 return {} 174 175 def requests_get(self, endpoint: str, params: Mapping = frozendict(), sudo=None): 176 combined_params = {} 177 combined_params.update(params) 178 if sudo: 179 combined_params["sudo"] = sudo.username 180 return self.parse_result(self.__get(endpoint, combined_params)) 181 182 def requests_get_raw(self, endpoint: str, params=frozendict(), sudo=None) -> bytes: 183 combined_params = {} 184 combined_params.update(params) 185 if sudo: 186 combined_params["sudo"] = sudo.username 187 return self.__get(endpoint, combined_params).content 188 189 def requests_get_paginated( 190 self, 191 endpoint: str, 192 params=frozendict(), 193 sudo=None, 194 page_key: str = "page", 195 first_page: int = 1, 196 ): 197 page = first_page 198 combined_params = {} 199 combined_params.update(params) 200 aggregated_result = [] 201 while True: 202 combined_params[page_key] = page 203 result = self.requests_get(endpoint, combined_params, sudo) 204 205 if not result: 206 return aggregated_result 207 208 if isinstance(result, dict): 209 if "data" in result: 210 data = result["data"] 211 if len(data) == 0: 212 return aggregated_result 213 aggregated_result.extend(data) 214 elif "tree" in result: 215 data = result["tree"] 216 if data is None or len(data) == 0: 217 return aggregated_result 218 aggregated_result.extend(data) 219 else: 220 raise NotImplementedError( 221 "requests_get_paginated does not know how to handle responses of this type." 222 ) 223 else: 224 aggregated_result.extend(result) 225 226 page += 1 227 228 def requests_put(self, endpoint: str, data: Optional[dict] = None): 229 if not data: 230 data = {} 231 response = self.requests.put( 232 self.__get_url(endpoint), headers=self.headers, data=json.dumps(data) 233 ) 234 if response.status_code not in [200, 204]: 235 message = ( 236 f"Received status code: {response.status_code} ({response.url}) {response.text}" 237 ) 238 self.logger.error(message) 239 raise Exception(message) 240 241 def requests_delete(self, endpoint: str, data: Optional[dict] = None): 242 response = self.requests.delete( 243 self.__get_url(endpoint), headers=self.headers, data=json.dumps(data) 244 ) 245 if response.status_code not in [200, 204]: 246 message = f"Received status code: {response.status_code} ({response.url})" 247 self.logger.error(message) 248 raise Exception(message) 249 250 def requests_post( 251 self, 252 endpoint: str, 253 data: Optional[dict] = None, 254 params: Optional[dict] = None, 255 files: Optional[dict] = None, 256 ): 257 """ 258 Make a POST call to the endpoint. 259 260 :param endpoint: The path to the endpoint 261 :param data: A dictionary for JSON data 262 :param params: A dictionary of query params 263 :param files: A dictionary of files, see requests.post. Using both files and data 264 can lead to unexpected results! 265 :return: The JSON response parsed as a dict 266 """ 267 268 # This should ideally be a TypedDict of the type of arguments taken by 269 # `requests.post`. 270 args: dict[str, Any] = { 271 "headers": self.headers.copy(), 272 } 273 if data is not None: 274 args["data"] = json.dumps(data) 275 if params is not None: 276 args["params"] = params 277 if files is not None: 278 args["headers"].pop("Content-type") 279 args["files"] = files 280 281 response = self.requests.post(self.__get_url(endpoint), **args) 282 283 if response.status_code not in [200, 201, 202]: 284 if "already exists" in response.text or "e-mail already in use" in response.text: 285 self.logger.warning(response.text) 286 raise AlreadyExistsException() 287 self.logger.error(f"Received status code: {response.status_code} ({response.url})") 288 self.logger.error(f"With info: {data} ({self.headers})") 289 self.logger.error(f"Answer: {response.text}") 290 raise Exception( 291 f"Received status code: {response.status_code} ({response.url}), {response.text}" 292 ) 293 return self.parse_result(response) 294 295 def requests_patch(self, endpoint: str, data: dict): 296 response = self.requests.patch( 297 self.__get_url(endpoint), headers=self.headers, data=json.dumps(data) 298 ) 299 if response.status_code not in [200, 201]: 300 error_message = f"Received status code: {response.status_code} ({response.url}) {data}" 301 self.logger.error(error_message) 302 raise Exception(error_message) 303 return self.parse_result(response) 304 305 def get_orgs_public_members_all(self, orgname): 306 path = "/orgs/" + orgname + "/public_members" 307 return self.requests_get(path) 308 309 def get_orgs(self): 310 path = "/admin/orgs" 311 results = self.requests_get(path) 312 return [Organization.parse_response(self, result) for result in results] 313 314 def get_user(self): 315 result = self.requests_get(AllSpice.GET_USER) 316 return User.parse_response(self, result) 317 318 def get_version(self) -> str: 319 result = self.requests_get(AllSpice.ALLSPICE_HUB_VERSION) 320 return result["version"] 321 322 def get_users(self) -> List[User]: 323 results = self.requests_get(AllSpice.GET_USERS_ADMIN) 324 return [User.parse_response(self, result) for result in results] 325 326 def get_user_by_email(self, email: str) -> Optional[User]: 327 users = self.get_users() 328 for user in users: 329 if user.email == email or email in user.emails: 330 return user 331 return None 332 333 def get_user_by_name(self, username: str) -> Optional[User]: 334 users = self.get_users() 335 for user in users: 336 if user.username == username: 337 return user 338 return None 339 340 def get_repository(self, owner: str, name: str) -> Repository: 341 path = self.GET_REPOSITORY.format(owner=owner, name=name) 342 result = self.requests_get(path) 343 return Repository.parse_response(self, result) 344 345 def create_user( 346 self, 347 user_name: str, 348 email: str, 349 password: str, 350 full_name: Optional[str] = None, 351 login_name: Optional[str] = None, 352 change_pw=True, 353 send_notify=True, 354 source_id=0, 355 ): 356 """Create User. 357 Throws: 358 AlreadyExistsException, if the User exists already 359 Exception, if something else went wrong. 360 """ 361 if not login_name: 362 login_name = user_name 363 if not full_name: 364 full_name = user_name 365 request_data = { 366 "source_id": source_id, 367 "login_name": login_name, 368 "full_name": full_name, 369 "username": user_name, 370 "email": email, 371 "password": password, 372 "send_notify": send_notify, 373 "must_change_password": change_pw, 374 } 375 376 self.logger.debug("Gitea post payload: %s", request_data) 377 result = self.requests_post(AllSpice.ADMIN_CREATE_USER, data=request_data) 378 if "id" in result: 379 self.logger.info( 380 "Successfully created User %s <%s> (id %s)", 381 result["login"], 382 result["email"], 383 result["id"], 384 ) 385 self.logger.debug("Gitea response: %s", result) 386 else: 387 self.logger.error(result["message"]) 388 raise Exception("User not created... (gitea: %s)" % result["message"]) 389 user = User.parse_response(self, result) 390 return user 391 392 def create_repo( 393 self, 394 repoOwner: Union[User, Organization], 395 repoName: str, 396 description: str = "", 397 private: bool = False, 398 autoInit=True, 399 gitignores: Optional[str] = None, 400 license: Optional[str] = None, 401 readme: str = "Default", 402 issue_labels: Optional[str] = None, 403 default_branch="master", 404 ): 405 """Create a Repository as the administrator 406 407 Throws: 408 AlreadyExistsException: If the Repository exists already. 409 Exception: If something else went wrong. 410 411 Note: 412 Non-admin users can not use this method. Please use instead 413 `allspice.User.create_repo` or `allspice.Organization.create_repo`. 414 """ 415 # although this only says user in the api, this also works for 416 # organizations 417 assert isinstance(repoOwner, User) or isinstance(repoOwner, Organization) 418 result = self.requests_post( 419 AllSpice.ADMIN_REPO_CREATE % repoOwner.username, 420 data={ 421 "name": repoName, 422 "description": description, 423 "private": private, 424 "auto_init": autoInit, 425 "gitignores": gitignores, 426 "license": license, 427 "issue_labels": issue_labels, 428 "readme": readme, 429 "default_branch": default_branch, 430 }, 431 ) 432 if "id" in result: 433 self.logger.info("Successfully created Repository %s " % result["name"]) 434 else: 435 self.logger.error(result["message"]) 436 raise Exception("Repository not created... (gitea: %s)" % result["message"]) 437 return Repository.parse_response(self, result) 438 439 def create_org( 440 self, 441 owner: User, 442 orgName: str, 443 description: str, 444 location="", 445 website="", 446 full_name="", 447 ): 448 assert isinstance(owner, User) 449 result = self.requests_post( 450 AllSpice.CREATE_ORG % owner.username, 451 data={ 452 "username": orgName, 453 "description": description, 454 "location": location, 455 "website": website, 456 "full_name": full_name, 457 }, 458 ) 459 if "id" in result: 460 self.logger.info("Successfully created Organization %s" % result["username"]) 461 else: 462 self.logger.error("Organization not created... (gitea: %s)" % result["message"]) 463 self.logger.error(result["message"]) 464 raise Exception("Organization not created... (gitea: %s)" % result["message"]) 465 return Organization.parse_response(self, result) 466 467 def create_team( 468 self, 469 org: Organization, 470 name: str, 471 description: str = "", 472 permission: str = "read", 473 can_create_org_repo: bool = False, 474 includes_all_repositories: bool = False, 475 units=( 476 "repo.code", 477 "repo.issues", 478 "repo.ext_issues", 479 "repo.wiki", 480 "repo.pulls", 481 "repo.releases", 482 "repo.ext_wiki", 483 ), 484 units_map={}, 485 ): 486 """Creates a Team. 487 488 Args: 489 org (Organization): Organization the Team will be part of. 490 name (str): The Name of the Team to be created. 491 description (str): Optional, None, short description of the new Team. 492 permission (str): Optional, 'read', What permissions the members 493 units_map (dict): Optional, {}, a mapping of units to their 494 permissions. If None or empty, the `permission` permission will 495 be applied to all units. Note: When both `units` and `units_map` 496 are given, `units_map` will be preferred. 497 """ 498 499 result = self.requests_post( 500 AllSpice.CREATE_TEAM % org.username, 501 data={ 502 "name": name, 503 "description": description, 504 "permission": permission, 505 "can_create_org_repo": can_create_org_repo, 506 "includes_all_repositories": includes_all_repositories, 507 "units": units, 508 "units_map": units_map, 509 }, 510 ) 511 512 if "id" in result: 513 self.logger.info("Successfully created Team %s" % result["name"]) 514 else: 515 self.logger.error("Team not created... (gitea: %s)" % result["message"]) 516 self.logger.error(result["message"]) 517 raise Exception("Team not created... (gitea: %s)" % result["message"]) 518 api_object = Team.parse_response(self, result) 519 setattr( 520 api_object, "_organization", org 521 ) # fixes strange behaviour of gitea not returning a valid organization here. 522 return api_object
Object to establish a session with AllSpice Hub.
55 def __init__( 56 self, 57 allspice_hub_url="https://hub.allspice.io", 58 token_text=None, 59 auth=None, 60 verify=True, 61 log_level="INFO", 62 ratelimiting=(100, 60), 63 retry: Union[Retry, int, None] = DEFAULT_RETRY, 64 use_new_schdoc_renderer: Optional[bool] = None, 65 ): 66 """Initializing an instance of the AllSpice Hub Client 67 68 Args: 69 allspice_hub_url (str): The URL for the AllSpice Hub instance. 70 Defaults to `https://hub.allspice.io`. 71 72 token_text (str, None): The access token, by default None. 73 74 auth (tuple, None): The user credentials 75 `(username, password)`, by default None. 76 77 verify (bool): If True, allow insecure server connections 78 when using SSL. 79 80 log_level (str): The log level, by default `INFO`. 81 82 ratelimiting (tuple[int, int], None): `(max_calls, period)`, 83 If None, no rate limiting is applied. By default, 100 calls 84 per minute are allowed. 85 86 retry (Retry, int, None): Set a retry policy for requests using a 87 urllib3 Retry object, an integer for the number of retries, or None 88 for no retries. This happens before py-allspice's own rate limiter. 89 The default is to retry all methods (even mutating methods) 90 only on 429s and 502s up to 6 times with exponential backoff 91 and jitter. 92 93 NOTE: AllSpice Hub responds with a 503 and a Retry-After header 94 when a generated file is not ready yet, and urllib3 retries any 95 503 that carries Retry-After. So with retries enabled, fetching 96 generated files transparently waits for generation instead of 97 raising NotYetGeneratedException, until retries are exhausted. 98 99 use_new_schdoc_renderer (bool): Allows explicit override for using the new Altium schematic renderer. If set, 100 this will take precedence over the default behavior on the AllSpice Hub instance. 101 """ 102 103 self.logger = logging.getLogger(__name__) 104 handler = logging.StreamHandler(sys.stderr) 105 handler.setFormatter( 106 logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 107 ) 108 self.logger.addHandler(handler) 109 self.logger.setLevel(log_level) 110 self.headers = { 111 "Content-type": "application/json", 112 } 113 self.url = allspice_hub_url 114 115 if ratelimiting is None: 116 self.requests = requests.Session() 117 else: 118 (max_calls, period) = ratelimiting 119 self.requests = RateLimitedSession(max_calls=max_calls, period=period) 120 121 if retry is not None: 122 adapter = HTTPAdapter(max_retries=retry) 123 self.requests.mount("https://", adapter) 124 self.requests.mount("http://", adapter) 125 126 # Manage authentification 127 if not token_text and not auth: 128 raise ValueError("Please provide auth or token_text, but not both") 129 if token_text: 130 self.headers["Authorization"] = "token " + token_text 131 if auth: 132 self.logger.warning( 133 "Using basic auth is not recommended. Prefer using a token instead." 134 ) 135 self.requests.auth = auth 136 137 # Manage SSL certification verification 138 self.requests.verify = verify 139 if not verify: 140 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 141 142 self.use_new_schdoc_renderer = use_new_schdoc_renderer
Initializing an instance of the AllSpice Hub Client
Args:
allspice_hub_url (str): The URL for the AllSpice Hub instance.
Defaults to https://hub.allspice.io.
token_text (str, None): The access token, by default None.
auth (tuple, None): The user credentials
`(username, password)`, by default None.
verify (bool): If True, allow insecure server connections
when using SSL.
log_level (str): The log level, by default `INFO`.
ratelimiting (tuple[int, int], None): `(max_calls, period)`,
If None, no rate limiting is applied. By default, 100 calls
per minute are allowed.
retry (Retry, int, None): Set a retry policy for requests using a
urllib3 Retry object, an integer for the number of retries, or None
for no retries. This happens before py-allspice's own rate limiter.
The default is to retry all methods (even mutating methods)
only on 429s and 502s up to 6 times with exponential backoff
and jitter.
NOTE: AllSpice Hub responds with a 503 and a Retry-After header
when a generated file is not ready yet, and urllib3 retries any
503 that carries Retry-After. So with retries enabled, fetching
generated files transparently waits for generation instead of
raising NotYetGeneratedException, until retries are exhausted.
use_new_schdoc_renderer (bool): Allows explicit override for using the new Altium schematic renderer. If set,
this will take precedence over the default behavior on the AllSpice Hub instance.
168 @staticmethod 169 def parse_result(result) -> Dict: 170 """Parses the result-JSON to a dict.""" 171 if result.text and len(result.text) > 3: 172 return json.loads(result.text) 173 return {}
Parses the result-JSON to a dict.
189 def requests_get_paginated( 190 self, 191 endpoint: str, 192 params=frozendict(), 193 sudo=None, 194 page_key: str = "page", 195 first_page: int = 1, 196 ): 197 page = first_page 198 combined_params = {} 199 combined_params.update(params) 200 aggregated_result = [] 201 while True: 202 combined_params[page_key] = page 203 result = self.requests_get(endpoint, combined_params, sudo) 204 205 if not result: 206 return aggregated_result 207 208 if isinstance(result, dict): 209 if "data" in result: 210 data = result["data"] 211 if len(data) == 0: 212 return aggregated_result 213 aggregated_result.extend(data) 214 elif "tree" in result: 215 data = result["tree"] 216 if data is None or len(data) == 0: 217 return aggregated_result 218 aggregated_result.extend(data) 219 else: 220 raise NotImplementedError( 221 "requests_get_paginated does not know how to handle responses of this type." 222 ) 223 else: 224 aggregated_result.extend(result) 225 226 page += 1
228 def requests_put(self, endpoint: str, data: Optional[dict] = None): 229 if not data: 230 data = {} 231 response = self.requests.put( 232 self.__get_url(endpoint), headers=self.headers, data=json.dumps(data) 233 ) 234 if response.status_code not in [200, 204]: 235 message = ( 236 f"Received status code: {response.status_code} ({response.url}) {response.text}" 237 ) 238 self.logger.error(message) 239 raise Exception(message)
241 def requests_delete(self, endpoint: str, data: Optional[dict] = None): 242 response = self.requests.delete( 243 self.__get_url(endpoint), headers=self.headers, data=json.dumps(data) 244 ) 245 if response.status_code not in [200, 204]: 246 message = f"Received status code: {response.status_code} ({response.url})" 247 self.logger.error(message) 248 raise Exception(message)
250 def requests_post( 251 self, 252 endpoint: str, 253 data: Optional[dict] = None, 254 params: Optional[dict] = None, 255 files: Optional[dict] = None, 256 ): 257 """ 258 Make a POST call to the endpoint. 259 260 :param endpoint: The path to the endpoint 261 :param data: A dictionary for JSON data 262 :param params: A dictionary of query params 263 :param files: A dictionary of files, see requests.post. Using both files and data 264 can lead to unexpected results! 265 :return: The JSON response parsed as a dict 266 """ 267 268 # This should ideally be a TypedDict of the type of arguments taken by 269 # `requests.post`. 270 args: dict[str, Any] = { 271 "headers": self.headers.copy(), 272 } 273 if data is not None: 274 args["data"] = json.dumps(data) 275 if params is not None: 276 args["params"] = params 277 if files is not None: 278 args["headers"].pop("Content-type") 279 args["files"] = files 280 281 response = self.requests.post(self.__get_url(endpoint), **args) 282 283 if response.status_code not in [200, 201, 202]: 284 if "already exists" in response.text or "e-mail already in use" in response.text: 285 self.logger.warning(response.text) 286 raise AlreadyExistsException() 287 self.logger.error(f"Received status code: {response.status_code} ({response.url})") 288 self.logger.error(f"With info: {data} ({self.headers})") 289 self.logger.error(f"Answer: {response.text}") 290 raise Exception( 291 f"Received status code: {response.status_code} ({response.url}), {response.text}" 292 ) 293 return self.parse_result(response)
Make a POST call to the endpoint.
Parameters
- endpoint: The path to the endpoint
- data: A dictionary for JSON data
- params: A dictionary of query params
- files: A dictionary of files, see requests.post. Using both files and data can lead to unexpected results!
Returns
The JSON response parsed as a dict
295 def requests_patch(self, endpoint: str, data: dict): 296 response = self.requests.patch( 297 self.__get_url(endpoint), headers=self.headers, data=json.dumps(data) 298 ) 299 if response.status_code not in [200, 201]: 300 error_message = f"Received status code: {response.status_code} ({response.url}) {data}" 301 self.logger.error(error_message) 302 raise Exception(error_message) 303 return self.parse_result(response)
345 def create_user( 346 self, 347 user_name: str, 348 email: str, 349 password: str, 350 full_name: Optional[str] = None, 351 login_name: Optional[str] = None, 352 change_pw=True, 353 send_notify=True, 354 source_id=0, 355 ): 356 """Create User. 357 Throws: 358 AlreadyExistsException, if the User exists already 359 Exception, if something else went wrong. 360 """ 361 if not login_name: 362 login_name = user_name 363 if not full_name: 364 full_name = user_name 365 request_data = { 366 "source_id": source_id, 367 "login_name": login_name, 368 "full_name": full_name, 369 "username": user_name, 370 "email": email, 371 "password": password, 372 "send_notify": send_notify, 373 "must_change_password": change_pw, 374 } 375 376 self.logger.debug("Gitea post payload: %s", request_data) 377 result = self.requests_post(AllSpice.ADMIN_CREATE_USER, data=request_data) 378 if "id" in result: 379 self.logger.info( 380 "Successfully created User %s <%s> (id %s)", 381 result["login"], 382 result["email"], 383 result["id"], 384 ) 385 self.logger.debug("Gitea response: %s", result) 386 else: 387 self.logger.error(result["message"]) 388 raise Exception("User not created... (gitea: %s)" % result["message"]) 389 user = User.parse_response(self, result) 390 return user
Create User. Throws: AlreadyExistsException, if the User exists already Exception, if something else went wrong.
392 def create_repo( 393 self, 394 repoOwner: Union[User, Organization], 395 repoName: str, 396 description: str = "", 397 private: bool = False, 398 autoInit=True, 399 gitignores: Optional[str] = None, 400 license: Optional[str] = None, 401 readme: str = "Default", 402 issue_labels: Optional[str] = None, 403 default_branch="master", 404 ): 405 """Create a Repository as the administrator 406 407 Throws: 408 AlreadyExistsException: If the Repository exists already. 409 Exception: If something else went wrong. 410 411 Note: 412 Non-admin users can not use this method. Please use instead 413 `allspice.User.create_repo` or `allspice.Organization.create_repo`. 414 """ 415 # although this only says user in the api, this also works for 416 # organizations 417 assert isinstance(repoOwner, User) or isinstance(repoOwner, Organization) 418 result = self.requests_post( 419 AllSpice.ADMIN_REPO_CREATE % repoOwner.username, 420 data={ 421 "name": repoName, 422 "description": description, 423 "private": private, 424 "auto_init": autoInit, 425 "gitignores": gitignores, 426 "license": license, 427 "issue_labels": issue_labels, 428 "readme": readme, 429 "default_branch": default_branch, 430 }, 431 ) 432 if "id" in result: 433 self.logger.info("Successfully created Repository %s " % result["name"]) 434 else: 435 self.logger.error(result["message"]) 436 raise Exception("Repository not created... (gitea: %s)" % result["message"]) 437 return Repository.parse_response(self, result)
Create a Repository as the administrator
Throws: AlreadyExistsException: If the Repository exists already. Exception: If something else went wrong.
Note:
Non-admin users can not use this method. Please use instead
allspice.User.create_repo or allspice.Organization.create_repo.
439 def create_org( 440 self, 441 owner: User, 442 orgName: str, 443 description: str, 444 location="", 445 website="", 446 full_name="", 447 ): 448 assert isinstance(owner, User) 449 result = self.requests_post( 450 AllSpice.CREATE_ORG % owner.username, 451 data={ 452 "username": orgName, 453 "description": description, 454 "location": location, 455 "website": website, 456 "full_name": full_name, 457 }, 458 ) 459 if "id" in result: 460 self.logger.info("Successfully created Organization %s" % result["username"]) 461 else: 462 self.logger.error("Organization not created... (gitea: %s)" % result["message"]) 463 self.logger.error(result["message"]) 464 raise Exception("Organization not created... (gitea: %s)" % result["message"]) 465 return Organization.parse_response(self, result)
467 def create_team( 468 self, 469 org: Organization, 470 name: str, 471 description: str = "", 472 permission: str = "read", 473 can_create_org_repo: bool = False, 474 includes_all_repositories: bool = False, 475 units=( 476 "repo.code", 477 "repo.issues", 478 "repo.ext_issues", 479 "repo.wiki", 480 "repo.pulls", 481 "repo.releases", 482 "repo.ext_wiki", 483 ), 484 units_map={}, 485 ): 486 """Creates a Team. 487 488 Args: 489 org (Organization): Organization the Team will be part of. 490 name (str): The Name of the Team to be created. 491 description (str): Optional, None, short description of the new Team. 492 permission (str): Optional, 'read', What permissions the members 493 units_map (dict): Optional, {}, a mapping of units to their 494 permissions. If None or empty, the `permission` permission will 495 be applied to all units. Note: When both `units` and `units_map` 496 are given, `units_map` will be preferred. 497 """ 498 499 result = self.requests_post( 500 AllSpice.CREATE_TEAM % org.username, 501 data={ 502 "name": name, 503 "description": description, 504 "permission": permission, 505 "can_create_org_repo": can_create_org_repo, 506 "includes_all_repositories": includes_all_repositories, 507 "units": units, 508 "units_map": units_map, 509 }, 510 ) 511 512 if "id" in result: 513 self.logger.info("Successfully created Team %s" % result["name"]) 514 else: 515 self.logger.error("Team not created... (gitea: %s)" % result["message"]) 516 self.logger.error(result["message"]) 517 raise Exception("Team not created... (gitea: %s)" % result["message"]) 518 api_object = Team.parse_response(self, result) 519 setattr( 520 api_object, "_organization", org 521 ) # fixes strange behaviour of gitea not returning a valid organization here. 522 return api_object
Creates a Team.
Args:
org (Organization): Organization the Team will be part of.
name (str): The Name of the Team to be created.
description (str): Optional, None, short description of the new Team.
permission (str): Optional, 'read', What permissions the members
units_map (dict): Optional, {}, a mapping of units to their
permissions. If None or empty, the permission permission will
be applied to all units. Note: When both units and units_map
are given, units_map will be preferred.
Common base class for all non-exit exceptions.
423class Branch(ReadonlyApiObject): 424 commit: Dict[str, Optional[Union[str, Dict[str, str], Dict[str, Optional[Union[bool, str]]]]]] 425 effective_branch_protection_name: str 426 enable_status_check: bool 427 name: str 428 protected: bool 429 required_approvals: int 430 status_check_contexts: List[Any] 431 user_can_merge: bool 432 user_can_push: bool 433 434 API_OBJECT = """/repos/{owner}/{repo}/branches/{branch}""" 435 436 def __init__(self, allspice_client): 437 super().__init__(allspice_client) 438 439 def __eq__(self, other): 440 if not isinstance(other, Branch): 441 return False 442 return self.commit == other.commit and self.name == other.name 443 444 def __hash__(self): 445 return hash(self.commit["id"]) ^ hash(self.name) 446 447 _fields_to_parsers: ClassVar[dict] = { 448 # This is not a commit object 449 # "commit": lambda allspice_client, c: Commit.parse_response(allspice_client, c) 450 } 451 452 @classmethod 453 def request(cls, allspice_client, owner: str, repo: str, branch: str): 454 return cls._request(allspice_client, {"owner": owner, "repo": repo, "branch": branch})
Inherited Members
1623class Comment(ApiObject): 1624 assets: List[Union[Any, Dict[str, Union[int, str]]]] 1625 body: str 1626 created_at: datetime 1627 html_url: str 1628 id: int 1629 issue_url: str 1630 original_author: str 1631 original_author_id: int 1632 pull_request_url: str 1633 updated_at: datetime 1634 user: User 1635 1636 API_OBJECT = """/repos/{owner}/{repo}/issues/comments/{id}""" 1637 GET_ATTACHMENTS_PATH = """/repos/{owner}/{repo}/issues/comments/{id}/assets""" 1638 ATTACHMENT_PATH = """/repos/{owner}/{repo}/issues/comments/{id}/assets/{attachment_id}""" 1639 1640 def __init__(self, allspice_client): 1641 super().__init__(allspice_client) 1642 1643 def __eq__(self, other): 1644 if not isinstance(other, Comment): 1645 return False 1646 return self.repository == other.repository and self.id == other.id 1647 1648 def __hash__(self): 1649 return hash(self.repository) ^ hash(self.id) 1650 1651 @classmethod 1652 def request(cls, allspice_client, owner: str, repo: str, id: str) -> "Comment": 1653 return cls._request(allspice_client, {"owner": owner, "repo": repo, "id": id}) 1654 1655 _fields_to_parsers: ClassVar[dict] = { 1656 "user": lambda allspice_client, r: User.parse_response(allspice_client, r), 1657 "created_at": lambda _, t: Util.convert_time(t), 1658 "updated_at": lambda _, t: Util.convert_time(t), 1659 } 1660 1661 _patchable_fields: ClassVar[set[str]] = {"body"} 1662 1663 @property 1664 def parent_url(self) -> str: 1665 """URL of the parent of this comment (the issue or the pull request)""" 1666 1667 if self.issue_url is not None and self.issue_url != "": 1668 return self.issue_url 1669 else: 1670 return self.pull_request_url 1671 1672 @cached_property 1673 def repository(self) -> Repository: 1674 """The repository this comment was posted on.""" 1675 1676 owner_name, repo_name = self.parent_url.split("/")[-4:-2] 1677 return Repository.request(self.allspice_client, owner_name, repo_name) 1678 1679 def __fields_for_path(self): 1680 return { 1681 "owner": self.repository.owner.username, 1682 "repo": self.repository.name, 1683 "id": self.id, 1684 } 1685 1686 def commit(self): 1687 self._commit(self.__fields_for_path()) 1688 1689 def delete(self): 1690 self.allspice_client.requests_delete(self.API_OBJECT.format(**self.__fields_for_path())) 1691 self.deleted = True 1692 1693 def get_attachments(self) -> List[Attachment]: 1694 """ 1695 Get all attachments on this comment. This returns Attachment objects, which 1696 contain a link to download the attachment. 1697 1698 https://hub.allspice.io/api/swagger#/issue/issueListIssueCommentAttachments 1699 """ 1700 1701 results = self.allspice_client.requests_get( 1702 self.GET_ATTACHMENTS_PATH.format(**self.__fields_for_path()) 1703 ) 1704 return [Attachment.parse_response(self.allspice_client, result) for result in results] 1705 1706 def create_attachment(self, file: IO, name: Optional[str] = None) -> Attachment: 1707 """ 1708 Create an attachment on this comment. 1709 1710 https://hub.allspice.io/api/swagger#/issue/issueCreateIssueCommentAttachment 1711 1712 :param file: The file to attach. This should be a file-like object. 1713 :param name: The name of the file. If not provided, the name of the file will be 1714 used. 1715 :return: The created attachment. 1716 """ 1717 1718 args: dict[str, Any] = { 1719 "files": {"attachment": file}, 1720 } 1721 if name is not None: 1722 args["params"] = {"name": name} 1723 1724 result = self.allspice_client.requests_post( 1725 self.GET_ATTACHMENTS_PATH.format(**self.__fields_for_path()), 1726 **args, 1727 ) 1728 return Attachment.parse_response(self.allspice_client, result) 1729 1730 def edit_attachment(self, attachment: Attachment, data: dict) -> Attachment: 1731 """ 1732 Edit an attachment. 1733 1734 The list of params that can be edited is available at 1735 https://hub.allspice.io/api/swagger#/issue/issueEditIssueCommentAttachment 1736 1737 :param attachment: The attachment to be edited 1738 :param data: The data parameter should be a dictionary of the fields to edit. 1739 :return: The edited attachment 1740 """ 1741 1742 args = { 1743 **self.__fields_for_path(), 1744 "attachment_id": attachment.id, 1745 } 1746 result = self.allspice_client.requests_patch( 1747 self.ATTACHMENT_PATH.format(**args), 1748 data=data, 1749 ) 1750 return Attachment.parse_response(self.allspice_client, result) 1751 1752 def delete_attachment(self, attachment: Attachment): 1753 """https://hub.allspice.io/api/swagger#/issue/issueDeleteIssueCommentAttachment""" 1754 1755 args = { 1756 **self.__fields_for_path(), 1757 "attachment_id": attachment.id, 1758 } 1759 self.allspice_client.requests_delete(self.ATTACHMENT_PATH.format(**args)) 1760 attachment.deleted = True
1663 @property 1664 def parent_url(self) -> str: 1665 """URL of the parent of this comment (the issue or the pull request)""" 1666 1667 if self.issue_url is not None and self.issue_url != "": 1668 return self.issue_url 1669 else: 1670 return self.pull_request_url
URL of the parent of this comment (the issue or the pull request)
1672 @cached_property 1673 def repository(self) -> Repository: 1674 """The repository this comment was posted on.""" 1675 1676 owner_name, repo_name = self.parent_url.split("/")[-4:-2] 1677 return Repository.request(self.allspice_client, owner_name, repo_name)
The repository this comment was posted on.
1693 def get_attachments(self) -> List[Attachment]: 1694 """ 1695 Get all attachments on this comment. This returns Attachment objects, which 1696 contain a link to download the attachment. 1697 1698 https://hub.allspice.io/api/swagger#/issue/issueListIssueCommentAttachments 1699 """ 1700 1701 results = self.allspice_client.requests_get( 1702 self.GET_ATTACHMENTS_PATH.format(**self.__fields_for_path()) 1703 ) 1704 return [Attachment.parse_response(self.allspice_client, result) for result in results]
Get all attachments on this comment. This returns Attachment objects, which contain a link to download the attachment.
allspice.allspice.io/api/swagger#/issue/issueListIssueCommentAttachments">https://huballspice.allspice.io/api/swagger#/issue/issueListIssueCommentAttachments
1706 def create_attachment(self, file: IO, name: Optional[str] = None) -> Attachment: 1707 """ 1708 Create an attachment on this comment. 1709 1710 https://hub.allspice.io/api/swagger#/issue/issueCreateIssueCommentAttachment 1711 1712 :param file: The file to attach. This should be a file-like object. 1713 :param name: The name of the file. If not provided, the name of the file will be 1714 used. 1715 :return: The created attachment. 1716 """ 1717 1718 args: dict[str, Any] = { 1719 "files": {"attachment": file}, 1720 } 1721 if name is not None: 1722 args["params"] = {"name": name} 1723 1724 result = self.allspice_client.requests_post( 1725 self.GET_ATTACHMENTS_PATH.format(**self.__fields_for_path()), 1726 **args, 1727 ) 1728 return Attachment.parse_response(self.allspice_client, result)
Create an attachment on this comment.
allspice.allspice.io/api/swagger#/issue/issueCreateIssueCommentAttachment">https://huballspice.allspice.io/api/swagger#/issue/issueCreateIssueCommentAttachment
Parameters
- file: The file to attach. This should be a file-like object.
- name: The name of the file. If not provided, the name of the file will be used.
Returns
The created attachment.
1730 def edit_attachment(self, attachment: Attachment, data: dict) -> Attachment: 1731 """ 1732 Edit an attachment. 1733 1734 The list of params that can be edited is available at 1735 https://hub.allspice.io/api/swagger#/issue/issueEditIssueCommentAttachment 1736 1737 :param attachment: The attachment to be edited 1738 :param data: The data parameter should be a dictionary of the fields to edit. 1739 :return: The edited attachment 1740 """ 1741 1742 args = { 1743 **self.__fields_for_path(), 1744 "attachment_id": attachment.id, 1745 } 1746 result = self.allspice_client.requests_patch( 1747 self.ATTACHMENT_PATH.format(**args), 1748 data=data, 1749 ) 1750 return Attachment.parse_response(self.allspice_client, result)
Edit an attachment.
The list of params that can be edited is available at allspice.allspice.io/api/swagger#/issue/issueEditIssueCommentAttachment">https://huballspice.allspice.io/api/swagger#/issue/issueEditIssueCommentAttachment
Parameters
- attachment: The attachment to be edited
- data: The data parameter should be a dictionary of the fields to edit.
Returns
The edited attachment
1752 def delete_attachment(self, attachment: Attachment): 1753 """https://hub.allspice.io/api/swagger#/issue/issueDeleteIssueCommentAttachment""" 1754 1755 args = { 1756 **self.__fields_for_path(), 1757 "attachment_id": attachment.id, 1758 } 1759 self.allspice_client.requests_delete(self.ATTACHMENT_PATH.format(**args)) 1760 attachment.deleted = True
allspice.allspice.io/api/swagger#/issue/issueDeleteIssueCommentAttachment">https://huballspice.allspice.io/api/swagger#/issue/issueDeleteIssueCommentAttachment
1763class Commit(ReadonlyApiObject): 1764 author: User 1765 commit: Dict[str, Union[str, Dict[str, str], Dict[str, Optional[Union[bool, str]]]]] 1766 committer: Dict[str, Union[int, str, bool]] 1767 created: str 1768 files: List[Dict[str, str]] 1769 html_url: str 1770 inner_commit: Dict[str, Union[str, Dict[str, str], Dict[str, Optional[Union[bool, str]]]]] 1771 parents: List[Union[Dict[str, str], Any]] 1772 sha: str 1773 stats: Dict[str, int] 1774 url: str 1775 1776 API_OBJECT = """/repos/{owner}/{repo}/commits/{sha}""" 1777 COMMIT_GET_STATUS = """/repos/{owner}/{repo}/commits/{sha}/status""" 1778 COMMIT_GET_STATUSES = """/repos/{owner}/{repo}/commits/{sha}/statuses""" 1779 1780 # Regex to extract owner and repo names from the url property 1781 URL_REGEXP = re.compile(r"/repos/([^/]+)/([^/]+)/git/commits") 1782 1783 def __init__(self, allspice_client): 1784 super().__init__(allspice_client) 1785 1786 _fields_to_parsers: ClassVar[dict] = { 1787 # NOTE: api may return None for commiters that are no allspice users 1788 "author": lambda allspice_client, u: User.parse_response(allspice_client, u) if u else None 1789 } 1790 1791 def __eq__(self, other): 1792 if not isinstance(other, Commit): 1793 return False 1794 return self.sha == other.sha 1795 1796 def __hash__(self): 1797 return hash(self.sha) 1798 1799 @classmethod 1800 def parse_response(cls, allspice_client, result) -> "Commit": 1801 commit_cache = result["commit"] 1802 api_object = cls(allspice_client) 1803 cls._initialize(allspice_client, api_object, result) 1804 # inner_commit for legacy reasons 1805 Commit._add_read_property("inner_commit", commit_cache, api_object) 1806 return api_object 1807 1808 def get_status(self) -> CommitCombinedStatus: 1809 """ 1810 Get a combined status consisting of all statues on this commit. 1811 1812 Note that the returned object is a CommitCombinedStatus object, which 1813 also contains a list of all statuses on the commit. 1814 1815 https://hub.allspice.io/api/swagger#/repository/repoGetCommitStatus 1816 """ 1817 1818 result = self.allspice_client.requests_get( 1819 self.COMMIT_GET_STATUS.format(**self._fields_for_path) 1820 ) 1821 return CommitCombinedStatus.parse_response(self.allspice_client, result) 1822 1823 def get_statuses(self) -> List[CommitStatus]: 1824 """ 1825 Get a list of all statuses on this commit. 1826 1827 https://hub.allspice.io/api/swagger#/repository/repoListCommitStatuses 1828 """ 1829 1830 results = self.allspice_client.requests_get( 1831 self.COMMIT_GET_STATUSES.format(**self._fields_for_path) 1832 ) 1833 return [CommitStatus.parse_response(self.allspice_client, result) for result in results] 1834 1835 @cached_property 1836 def _fields_for_path(self) -> dict[str, str]: 1837 matches = self.URL_REGEXP.search(self.url) 1838 if not matches: 1839 raise ValueError(f"Invalid commit URL: {self.url}") 1840 1841 return { 1842 "owner": matches.group(1), 1843 "repo": matches.group(2), 1844 "sha": self.sha, 1845 }
1799 @classmethod 1800 def parse_response(cls, allspice_client, result) -> "Commit": 1801 commit_cache = result["commit"] 1802 api_object = cls(allspice_client) 1803 cls._initialize(allspice_client, api_object, result) 1804 # inner_commit for legacy reasons 1805 Commit._add_read_property("inner_commit", commit_cache, api_object) 1806 return api_object
1808 def get_status(self) -> CommitCombinedStatus: 1809 """ 1810 Get a combined status consisting of all statues on this commit. 1811 1812 Note that the returned object is a CommitCombinedStatus object, which 1813 also contains a list of all statuses on the commit. 1814 1815 https://hub.allspice.io/api/swagger#/repository/repoGetCommitStatus 1816 """ 1817 1818 result = self.allspice_client.requests_get( 1819 self.COMMIT_GET_STATUS.format(**self._fields_for_path) 1820 ) 1821 return CommitCombinedStatus.parse_response(self.allspice_client, result)
Get a combined status consisting of all statues on this commit.
Note that the returned object is a CommitCombinedStatus object, which also contains a list of all statuses on the commit.
allspice.allspice.io/api/swagger#/repository/repoGetCommitStatus">https://huballspice.allspice.io/api/swagger#/repository/repoGetCommitStatus
1823 def get_statuses(self) -> List[CommitStatus]: 1824 """ 1825 Get a list of all statuses on this commit. 1826 1827 https://hub.allspice.io/api/swagger#/repository/repoListCommitStatuses 1828 """ 1829 1830 results = self.allspice_client.requests_get( 1831 self.COMMIT_GET_STATUSES.format(**self._fields_for_path) 1832 ) 1833 return [CommitStatus.parse_response(self.allspice_client, result) for result in results]
Get a list of all statuses on this commit.
allspice.allspice.io/api/swagger#/repository/repoListCommitStatuses">https://huballspice.allspice.io/api/swagger#/repository/repoListCommitStatuses
Inherited Members
2944class Content(ReadonlyApiObject): 2945 content: Any 2946 download_url: str 2947 encoding: Any 2948 git_url: str 2949 html_url: str 2950 last_author_date: str 2951 last_commit_sha: str 2952 last_committer_date: str 2953 name: str 2954 path: str 2955 sha: str 2956 size: int 2957 submodule_git_url: Any 2958 target: Any 2959 type: str 2960 url: str 2961 2962 FILE = "file" 2963 2964 def __init__(self, allspice_client): 2965 super().__init__(allspice_client) 2966 2967 def __eq__(self, other): 2968 if not isinstance(other, Content): 2969 return False 2970 2971 return self.sha == other.sha and self.name == other.name 2972 2973 def __hash__(self): 2974 return hash(self.sha) ^ hash(self.name)
Inherited Members
2318class DesignReview(ApiObject): 2319 """ 2320 A Design Review. See 2321 https://hub.allspice.io/api/swagger#/repository/repoGetPullRequest. 2322 2323 Note: The base and head fields are not `Branch` objects - they are plain strings 2324 referring to the branch names. This is because DRs can exist for branches that have 2325 been deleted, which don't have an associated `Branch` object from the API. You can use 2326 the `Repository.get_branch` method to get a `Branch` object for a branch if you know 2327 it exists. 2328 """ 2329 2330 additions: Optional[int] 2331 allow_maintainer_edit: bool 2332 allow_maintainer_edits: Any 2333 assignee: User 2334 assignees: List["User"] 2335 base: str 2336 body: str 2337 changed_files: Optional[int] 2338 closed_at: Optional[str] 2339 comments: int 2340 created_at: str 2341 deletions: Optional[int] 2342 diff_url: str 2343 draft: bool 2344 due_date: Optional[str] 2345 head: str 2346 html_url: str 2347 id: int 2348 is_locked: bool 2349 labels: List[Any] 2350 merge_base: str 2351 merge_commit_sha: Optional[str] 2352 mergeable: bool 2353 merged: bool 2354 merged_at: Optional[str] 2355 merged_by: Any 2356 milestone: Any 2357 number: int 2358 patch_url: str 2359 pin_order: int 2360 repository: Optional["Repository"] 2361 requested_reviewers: Any 2362 requested_reviewers_teams: Any 2363 review_comments: Optional[int] 2364 state: str 2365 title: str 2366 updated_at: str 2367 url: str 2368 user: User 2369 2370 API_OBJECT = "/repos/{owner}/{repo}/pulls/{index}" 2371 MERGE_DESIGN_REVIEW = "/repos/{owner}/{repo}/pulls/{index}/merge" 2372 GET_REVIEWS = "/repos/{owner}/{repo}/pulls/{index}/reviews" 2373 GET_REVIEW = "/repos/{owner}/{repo}/pulls/{index}/reviews/{review_id}" 2374 GET_COMMENTS = "/repos/{owner}/{repo}/issues/{index}/comments" 2375 2376 OPEN = "open" 2377 CLOSED = "closed" 2378 2379 class MergeType(Enum): 2380 MERGE = "merge" 2381 REBASE = "rebase" 2382 REBASE_MERGE = "rebase-merge" 2383 SQUASH = "squash" 2384 MANUALLY_MERGED = "manually-merged" 2385 2386 def __init__(self, allspice_client): 2387 super().__init__(allspice_client) 2388 2389 def __eq__(self, other): 2390 if not isinstance(other, DesignReview): 2391 return False 2392 return self.repository == other.repository and self.id == other.id 2393 2394 def __hash__(self): 2395 return hash(self.repository) ^ hash(self.id) 2396 2397 @classmethod 2398 def parse_response(cls, allspice_client, result) -> "DesignReview": 2399 api_object = super().parse_response(allspice_client, result) 2400 cls._add_read_property( 2401 "repository", 2402 Repository.parse_response(allspice_client, result["base"]["repo"]), 2403 api_object, 2404 ) 2405 2406 return api_object 2407 2408 @classmethod 2409 def request(cls, allspice_client, owner: str, repo: str, number: str): 2410 """See https://hub.allspice.io/api/swagger#/repository/repoGetPullRequest""" 2411 return cls._request(allspice_client, {"owner": owner, "repo": repo, "index": number}) 2412 2413 _fields_to_parsers: ClassVar[dict] = { 2414 "assignee": lambda allspice_client, u: User.parse_response(allspice_client, u), 2415 "assignees": lambda allspice_client, us: [ 2416 User.parse_response(allspice_client, u) for u in us 2417 ], 2418 "base": lambda _, b: b["ref"], 2419 "head": lambda _, h: h["ref"], 2420 "merged_by": lambda allspice_client, u: User.parse_response(allspice_client, u), 2421 "milestone": lambda allspice_client, m: Milestone.parse_response(allspice_client, m), 2422 "user": lambda allspice_client, u: User.parse_response(allspice_client, u), 2423 } 2424 2425 _patchable_fields: ClassVar[set[str]] = { 2426 "allow_maintainer_edits", 2427 "assignee", 2428 "assignees", 2429 "base", 2430 "body", 2431 "due_date", 2432 "milestone", 2433 "state", 2434 "title", 2435 } 2436 2437 _parsers_to_fields: ClassVar[dict] = { 2438 "assignee": lambda u: u.username, 2439 "assignees": lambda us: [u.username for u in us], 2440 "base": lambda b: b.name if isinstance(b, Branch) else b, 2441 "milestone": lambda m: m.id, 2442 } 2443 2444 def commit(self): 2445 data = self.get_dirty_fields() 2446 if "due_date" in data and data["due_date"] is None: 2447 data["unset_due_date"] = True 2448 2449 args = { 2450 "owner": self.repository.owner.username, 2451 "repo": self.repository.name, 2452 "index": self.number, 2453 } 2454 self._commit(args, data) 2455 2456 def merge(self, merge_type: MergeType): 2457 """ 2458 Merge the pull request. See 2459 https://hub.allspice.io/api/swagger#/repository/repoMergePullRequest 2460 2461 :param merge_type: The type of merge to perform. See the MergeType enum. 2462 """ 2463 2464 self.allspice_client.requests_post( 2465 self.MERGE_DESIGN_REVIEW.format( 2466 owner=self.repository.owner.username, 2467 repo=self.repository.name, 2468 index=self.number, 2469 ), 2470 data={"Do": merge_type.value}, 2471 ) 2472 2473 def get_comments(self) -> List[Comment]: 2474 """ 2475 Get the comments on this pull request, but not specifically on a review. 2476 2477 https://hub.allspice.io/api/swagger#/issue/issueGetComments 2478 2479 :return: A list of comments on this pull request. 2480 """ 2481 2482 results = self.allspice_client.requests_get( 2483 self.GET_COMMENTS.format( 2484 owner=self.repository.owner.username, 2485 repo=self.repository.name, 2486 index=self.number, 2487 ) 2488 ) 2489 return [Comment.parse_response(self.allspice_client, result) for result in results] 2490 2491 def create_comment(self, body: str): 2492 """ 2493 Create a comment on this pull request. This uses the same endpoint as the 2494 comments on issues, and will not be associated with any reviews. 2495 2496 https://hub.allspice.io/api/swagger#/issue/issueCreateComment 2497 2498 :param body: The body of the comment. 2499 :return: The comment that was created. 2500 """ 2501 2502 result = self.allspice_client.requests_post( 2503 self.GET_COMMENTS.format( 2504 owner=self.repository.owner.username, 2505 repo=self.repository.name, 2506 index=self.number, 2507 ), 2508 data={"body": body}, 2509 ) 2510 return Comment.parse_response(self.allspice_client, result) 2511 2512 def create_review( 2513 self, 2514 *, 2515 body: Optional[str] = None, 2516 event: Optional[DesignReviewReview.ReviewEvent] = None, 2517 comments: Optional[List[DesignReviewReview.ReviewComment]] = None, 2518 commit_id: Optional[str] = None, 2519 ) -> DesignReviewReview: 2520 """ 2521 Create a review on this design review. 2522 2523 https://hub.allspice.io/api/swagger#/repository/repoCreatePullRequestReview 2524 2525 Note: in most cases, you should not set the body or event when creating 2526 a review. The event is automatically set to "PENDING" when the review 2527 is created. You should then use `submit_review` to submit the review 2528 with the desired event and body. 2529 2530 :param body: The body of the review. This is the top-level comment on 2531 the review. If not provided, the review will be created with no body. 2532 :param event: The event of the review. This is the overall status of the 2533 review. See the ReviewEvent enum. If not provided, the API will 2534 default to "PENDING". 2535 :param comments: A list of comments on the review. Each comment should 2536 be a ReviewComment object. If not provided, only the base comment 2537 will be created. 2538 :param commit_id: The commit SHA to associate with the review. This is 2539 optional. 2540 """ 2541 2542 data: dict[str, Any] = {} 2543 2544 if body is not None: 2545 data["body"] = body 2546 if event is not None: 2547 data["event"] = event.value 2548 if commit_id is not None: 2549 data["commit_id"] = commit_id 2550 if comments is not None: 2551 data["comments"] = [asdict(comment) for comment in comments] 2552 2553 result = self.allspice_client.requests_post( 2554 self.GET_REVIEWS.format( 2555 owner=self.repository.owner.username, 2556 repo=self.repository.name, 2557 index=self.number, 2558 ), 2559 data=data, 2560 ) 2561 2562 return DesignReviewReview.parse_response(self.allspice_client, result) 2563 2564 def get_reviews(self) -> List[DesignReviewReview]: 2565 """ 2566 Get all reviews on this design review. 2567 2568 https://hub.allspice.io/api/swagger#/repository/repoListPullReviews 2569 """ 2570 2571 results = self.allspice_client.requests_get( 2572 self.GET_REVIEWS.format( 2573 owner=self.repository.owner.username, 2574 repo=self.repository.name, 2575 index=self.number, 2576 ) 2577 ) 2578 2579 return [ 2580 DesignReviewReview.parse_response(self.allspice_client, result) for result in results 2581 ] 2582 2583 def submit_review( 2584 self, 2585 review_id: int, 2586 event: DesignReviewReview.ReviewEvent, 2587 *, 2588 body: Optional[str] = None, 2589 ): 2590 """ 2591 Submit a review on this design review. 2592 2593 https://hub.allspice.io/api/swagger#/repository/repoSubmitPullReview 2594 2595 :param review_id: The ID of the review to submit. 2596 :param event: The event to submit the review with. See the ReviewEvent 2597 enum for the possible values. 2598 :param body: Optional body text for the review submission. 2599 """ 2600 2601 data = { 2602 "event": event.value, 2603 } 2604 if body is not None: 2605 data["body"] = body 2606 2607 result = self.allspice_client.requests_post( 2608 self.GET_REVIEW.format( 2609 owner=self.repository.owner.username, 2610 repo=self.repository.name, 2611 index=self.number, 2612 review_id=review_id, 2613 ), 2614 data=data, 2615 ) 2616 2617 return result
A Design Review. See allspice.allspice.io/api/swagger#/repository/repoGetPullRequest">https://huballspice.allspice.io/api/swagger#/repository/repoGetPullRequest.
Note: The base and head fields are not Branch objects - they are plain strings
referring to the branch names. This is because DRs can exist for branches that have
been deleted, which don't have an associated Branch object from the API. You can use
the Repository.get_branch method to get a Branch object for a branch if you know
it exists.
2397 @classmethod 2398 def parse_response(cls, allspice_client, result) -> "DesignReview": 2399 api_object = super().parse_response(allspice_client, result) 2400 cls._add_read_property( 2401 "repository", 2402 Repository.parse_response(allspice_client, result["base"]["repo"]), 2403 api_object, 2404 ) 2405 2406 return api_object
2408 @classmethod 2409 def request(cls, allspice_client, owner: str, repo: str, number: str): 2410 """See https://hub.allspice.io/api/swagger#/repository/repoGetPullRequest""" 2411 return cls._request(allspice_client, {"owner": owner, "repo": repo, "index": number})
See allspice.allspice.io/api/swagger#/repository/repoGetPullRequest">https://huballspice.allspice.io/api/swagger#/repository/repoGetPullRequest
2444 def commit(self): 2445 data = self.get_dirty_fields() 2446 if "due_date" in data and data["due_date"] is None: 2447 data["unset_due_date"] = True 2448 2449 args = { 2450 "owner": self.repository.owner.username, 2451 "repo": self.repository.name, 2452 "index": self.number, 2453 } 2454 self._commit(args, data)
2456 def merge(self, merge_type: MergeType): 2457 """ 2458 Merge the pull request. See 2459 https://hub.allspice.io/api/swagger#/repository/repoMergePullRequest 2460 2461 :param merge_type: The type of merge to perform. See the MergeType enum. 2462 """ 2463 2464 self.allspice_client.requests_post( 2465 self.MERGE_DESIGN_REVIEW.format( 2466 owner=self.repository.owner.username, 2467 repo=self.repository.name, 2468 index=self.number, 2469 ), 2470 data={"Do": merge_type.value}, 2471 )
Merge the pull request. See allspice.allspice.io/api/swagger#/repository/repoMergePullRequest">https://huballspice.allspice.io/api/swagger#/repository/repoMergePullRequest
Parameters
- merge_type: The type of merge to perform. See the MergeType enum.
2473 def get_comments(self) -> List[Comment]: 2474 """ 2475 Get the comments on this pull request, but not specifically on a review. 2476 2477 https://hub.allspice.io/api/swagger#/issue/issueGetComments 2478 2479 :return: A list of comments on this pull request. 2480 """ 2481 2482 results = self.allspice_client.requests_get( 2483 self.GET_COMMENTS.format( 2484 owner=self.repository.owner.username, 2485 repo=self.repository.name, 2486 index=self.number, 2487 ) 2488 ) 2489 return [Comment.parse_response(self.allspice_client, result) for result in results]
Get the comments on this pull request, but not specifically on a review.
allspice.allspice.io/api/swagger#/issue/issueGetComments">https://huballspice.allspice.io/api/swagger#/issue/issueGetComments
Returns
A list of comments on this pull request.
2491 def create_comment(self, body: str): 2492 """ 2493 Create a comment on this pull request. This uses the same endpoint as the 2494 comments on issues, and will not be associated with any reviews. 2495 2496 https://hub.allspice.io/api/swagger#/issue/issueCreateComment 2497 2498 :param body: The body of the comment. 2499 :return: The comment that was created. 2500 """ 2501 2502 result = self.allspice_client.requests_post( 2503 self.GET_COMMENTS.format( 2504 owner=self.repository.owner.username, 2505 repo=self.repository.name, 2506 index=self.number, 2507 ), 2508 data={"body": body}, 2509 ) 2510 return Comment.parse_response(self.allspice_client, result)
Create a comment on this pull request. This uses the same endpoint as the comments on issues, and will not be associated with any reviews.
allspice.allspice.io/api/swagger#/issue/issueCreateComment">https://huballspice.allspice.io/api/swagger#/issue/issueCreateComment
Parameters
- body: The body of the comment.
Returns
The comment that was created.
2512 def create_review( 2513 self, 2514 *, 2515 body: Optional[str] = None, 2516 event: Optional[DesignReviewReview.ReviewEvent] = None, 2517 comments: Optional[List[DesignReviewReview.ReviewComment]] = None, 2518 commit_id: Optional[str] = None, 2519 ) -> DesignReviewReview: 2520 """ 2521 Create a review on this design review. 2522 2523 https://hub.allspice.io/api/swagger#/repository/repoCreatePullRequestReview 2524 2525 Note: in most cases, you should not set the body or event when creating 2526 a review. The event is automatically set to "PENDING" when the review 2527 is created. You should then use `submit_review` to submit the review 2528 with the desired event and body. 2529 2530 :param body: The body of the review. This is the top-level comment on 2531 the review. If not provided, the review will be created with no body. 2532 :param event: The event of the review. This is the overall status of the 2533 review. See the ReviewEvent enum. If not provided, the API will 2534 default to "PENDING". 2535 :param comments: A list of comments on the review. Each comment should 2536 be a ReviewComment object. If not provided, only the base comment 2537 will be created. 2538 :param commit_id: The commit SHA to associate with the review. This is 2539 optional. 2540 """ 2541 2542 data: dict[str, Any] = {} 2543 2544 if body is not None: 2545 data["body"] = body 2546 if event is not None: 2547 data["event"] = event.value 2548 if commit_id is not None: 2549 data["commit_id"] = commit_id 2550 if comments is not None: 2551 data["comments"] = [asdict(comment) for comment in comments] 2552 2553 result = self.allspice_client.requests_post( 2554 self.GET_REVIEWS.format( 2555 owner=self.repository.owner.username, 2556 repo=self.repository.name, 2557 index=self.number, 2558 ), 2559 data=data, 2560 ) 2561 2562 return DesignReviewReview.parse_response(self.allspice_client, result)
Create a review on this design review.
allspice.allspice.io/api/swagger#/repository/repoCreatePullRequestReview">https://huballspice.allspice.io/api/swagger#/repository/repoCreatePullRequestReview
Note: in most cases, you should not set the body or event when creating
a review. The event is automatically set to "PENDING" when the review
is created. You should then use submit_review to submit the review
with the desired event and body.
Parameters
- body: The body of the review. This is the top-level comment on the review. If not provided, the review will be created with no body.
- event: The event of the review. This is the overall status of the review. See the ReviewEvent enum. If not provided, the API will default to "PENDING".
- comments: A list of comments on the review. Each comment should be a ReviewComment object. If not provided, only the base comment will be created.
- commit_id: The commit SHA to associate with the review. This is optional.
2564 def get_reviews(self) -> List[DesignReviewReview]: 2565 """ 2566 Get all reviews on this design review. 2567 2568 https://hub.allspice.io/api/swagger#/repository/repoListPullReviews 2569 """ 2570 2571 results = self.allspice_client.requests_get( 2572 self.GET_REVIEWS.format( 2573 owner=self.repository.owner.username, 2574 repo=self.repository.name, 2575 index=self.number, 2576 ) 2577 ) 2578 2579 return [ 2580 DesignReviewReview.parse_response(self.allspice_client, result) for result in results 2581 ]
Get all reviews on this design review.
allspice.allspice.io/api/swagger#/repository/repoListPullReviews">https://huballspice.allspice.io/api/swagger#/repository/repoListPullReviews
2583 def submit_review( 2584 self, 2585 review_id: int, 2586 event: DesignReviewReview.ReviewEvent, 2587 *, 2588 body: Optional[str] = None, 2589 ): 2590 """ 2591 Submit a review on this design review. 2592 2593 https://hub.allspice.io/api/swagger#/repository/repoSubmitPullReview 2594 2595 :param review_id: The ID of the review to submit. 2596 :param event: The event to submit the review with. See the ReviewEvent 2597 enum for the possible values. 2598 :param body: Optional body text for the review submission. 2599 """ 2600 2601 data = { 2602 "event": event.value, 2603 } 2604 if body is not None: 2605 data["body"] = body 2606 2607 result = self.allspice_client.requests_post( 2608 self.GET_REVIEW.format( 2609 owner=self.repository.owner.username, 2610 repo=self.repository.name, 2611 index=self.number, 2612 review_id=review_id, 2613 ), 2614 data=data, 2615 ) 2616 2617 return result
Submit a review on this design review.
allspice.allspice.io/api/swagger#/repository/repoSubmitPullReview">https://huballspice.allspice.io/api/swagger#/repository/repoSubmitPullReview
Parameters
- review_id: The ID of the review to submit.
- event: The event to submit the review with. See the ReviewEvent enum for the possible values.
- body: Optional body text for the review submission.
2176class DesignReviewReview(ReadonlyApiObject): 2177 """ 2178 A review on a Design Review. 2179 """ 2180 2181 body: str 2182 comments_count: int 2183 commit_id: str 2184 dismissed: bool 2185 html_url: str 2186 id: int 2187 official: bool 2188 pull_request_url: str 2189 stale: bool 2190 state: ReviewEvent 2191 submitted_at: str 2192 team: Any 2193 updated_at: str 2194 user: User 2195 2196 API_OBJECT = "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}" 2197 GET_COMMENTS = "/repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments" 2198 2199 class ReviewEvent(Enum): 2200 APPROVED = "APPROVED" 2201 PENDING = "PENDING" 2202 COMMENT = "COMMENT" 2203 REQUEST_CHANGES = "REQUEST_CHANGES" 2204 REQUEST_REVIEW = "REQUEST_REVIEW" 2205 UNKNOWN = "" 2206 2207 @dataclass 2208 class ReviewComment: 2209 """ 2210 Data required to create a review comment on a design review. 2211 2212 :param body: The body of the comment. 2213 :param path: The path of the file to comment on. If you have a 2214 `Content` object, get the path using the `path` property. 2215 :param sub_path: The sub-path of the file to comment on. This is 2216 usually the page ID of the page in the multi-page document. 2217 :param new_position: The line number of the source code file after the 2218 change to add this comment on. Optional, leave unset if this is an ECAD 2219 file or the comment must be on the entire file. 2220 :param old_position: The line number of the source code file before the 2221 change to add this comment on. Optional, leave unset if this is an ECAD 2222 file or the comment must be on the entire file. 2223 :param severity: The severity of the finding this comment reports, so 2224 readers can filter comments by it. Optional. AllSpice Hub rejects it 2225 unless the caller is DRCY or an Actions workflow, rejects a value 2226 other than "error", "warning", or "recommendation", and versions 2227 before severity support ignore it. 2228 """ 2229 2230 body: str 2231 path: str 2232 sub_path: Optional[str] = None 2233 new_position: Optional[int] = None 2234 old_position: Optional[int] = None 2235 severity: Optional[Literal["error", "warning", "recommendation"]] = None 2236 2237 def __init__(self, allspice_client): 2238 super().__init__(allspice_client) 2239 2240 _fields_to_parsers: ClassVar[dict] = { 2241 "user": lambda allspice_client, u: User.parse_response(allspice_client, u), 2242 "state": lambda _, s: DesignReviewReview.ReviewEvent(s), 2243 } 2244 2245 def _get_dr_properties(self) -> dict[str, str]: 2246 """ 2247 Get the owner, repo name and design review number from the URL of this 2248 review's DR. 2249 """ 2250 2251 parts = self.pull_request_url.strip("/").split("/") 2252 2253 try: 2254 index = parts[-1] 2255 assert parts[-2] == "pulls" or parts[-2] == "pull", ( 2256 "Expected the second last part of the URL to be 'pulls' or 'pull', " 2257 ) 2258 repo = parts[-3] 2259 owner = parts[-4] 2260 2261 return { 2262 "owner": owner, 2263 "repo": repo, 2264 "index": index, 2265 } 2266 except IndexError: 2267 raise ValueError("Malformed design review URL: {}".format(self.pull_request_url)) 2268 2269 @cached_property 2270 def owner_name(self) -> str: 2271 """ 2272 The owner of the repository this review is on. 2273 """ 2274 2275 return self._get_dr_properties()["owner"] 2276 2277 @cached_property 2278 def repository_name(self) -> str: 2279 """ 2280 The name of the repository this review is on. 2281 """ 2282 2283 return self._get_dr_properties()["repo"] 2284 2285 @cached_property 2286 def index(self) -> str: 2287 """ 2288 The index of the design review this review is on. 2289 """ 2290 2291 return self._get_dr_properties()["index"] 2292 2293 def delete(self): 2294 """ 2295 Delete this review. 2296 """ 2297 2298 self.allspice_client.requests_delete( 2299 self.API_OBJECT.format(**self._get_dr_properties(), id=self.id) 2300 ) 2301 self.deleted = True 2302 2303 def get_comments(self) -> List[DesignReviewReviewComment]: 2304 """ 2305 Get the comments on this review. 2306 """ 2307 2308 result = self.allspice_client.requests_get( 2309 self.GET_COMMENTS.format(**self._get_dr_properties(), id=self.id) 2310 ) 2311 2312 return [ 2313 DesignReviewReviewComment.parse_response(self.allspice_client, comment) 2314 for comment in result 2315 ]
A review on a Design Review.
2269 @cached_property 2270 def owner_name(self) -> str: 2271 """ 2272 The owner of the repository this review is on. 2273 """ 2274 2275 return self._get_dr_properties()["owner"]
The owner of the repository this review is on.
2277 @cached_property 2278 def repository_name(self) -> str: 2279 """ 2280 The name of the repository this review is on. 2281 """ 2282 2283 return self._get_dr_properties()["repo"]
The name of the repository this review is on.
2285 @cached_property 2286 def index(self) -> str: 2287 """ 2288 The index of the design review this review is on. 2289 """ 2290 2291 return self._get_dr_properties()["index"]
The index of the design review this review is on.
2293 def delete(self): 2294 """ 2295 Delete this review. 2296 """ 2297 2298 self.allspice_client.requests_delete( 2299 self.API_OBJECT.format(**self._get_dr_properties(), id=self.id) 2300 ) 2301 self.deleted = True
Delete this review.
2303 def get_comments(self) -> List[DesignReviewReviewComment]: 2304 """ 2305 Get the comments on this review. 2306 """ 2307 2308 result = self.allspice_client.requests_get( 2309 self.GET_COMMENTS.format(**self._get_dr_properties(), id=self.id) 2310 ) 2311 2312 return [ 2313 DesignReviewReviewComment.parse_response(self.allspice_client, comment) 2314 for comment in result 2315 ]
Get the comments on this review.
Inherited Members
2207 @dataclass 2208 class ReviewComment: 2209 """ 2210 Data required to create a review comment on a design review. 2211 2212 :param body: The body of the comment. 2213 :param path: The path of the file to comment on. If you have a 2214 `Content` object, get the path using the `path` property. 2215 :param sub_path: The sub-path of the file to comment on. This is 2216 usually the page ID of the page in the multi-page document. 2217 :param new_position: The line number of the source code file after the 2218 change to add this comment on. Optional, leave unset if this is an ECAD 2219 file or the comment must be on the entire file. 2220 :param old_position: The line number of the source code file before the 2221 change to add this comment on. Optional, leave unset if this is an ECAD 2222 file or the comment must be on the entire file. 2223 :param severity: The severity of the finding this comment reports, so 2224 readers can filter comments by it. Optional. AllSpice Hub rejects it 2225 unless the caller is DRCY or an Actions workflow, rejects a value 2226 other than "error", "warning", or "recommendation", and versions 2227 before severity support ignore it. 2228 """ 2229 2230 body: str 2231 path: str 2232 sub_path: Optional[str] = None 2233 new_position: Optional[int] = None 2234 old_position: Optional[int] = None 2235 severity: Optional[Literal["error", "warning", "recommendation"]] = None
Data required to create a review comment on a design review.
Parameters
- body: The body of the comment.
- path: The path of the file to comment on. If you have a
Contentobject, get the path using thepathproperty. - sub_path: The sub-path of the file to comment on. This is usually the page ID of the page in the multi-page document.
- new_position: The line number of the source code file after the change to add this comment on. Optional, leave unset if this is an ECAD file or the comment must be on the entire file.
- old_position: The line number of the source code file before the change to add this comment on. Optional, leave unset if this is an ECAD file or the comment must be on the entire file.
- severity: The severity of the finding this comment reports, so readers can filter comments by it. Optional. AllSpice Hub rejects it unless the caller is DRCY or an Actions workflow, rejects a value other than "error", "warning", or "recommendation", and versions before severity support ignore it.
35class InternalServerException(Exception): 36 def __init__(self, message: str, body: Optional[APIError] = None): 37 super().__init__(message) 38 self.body = body
Common base class for all non-exit exceptions.
1936class Issue(ApiObject): 1937 """ 1938 An issue on a repository. 1939 1940 Note: `Issue.assets` may not have any entries even if the issue has 1941 attachments. This happens when an issue is fetched via a bulk method like 1942 `Repository.get_issues`. In most cases, prefer using 1943 `Issue.get_attachments` to get the attachments on an issue. 1944 """ 1945 1946 assets: List[Union[Any, "Attachment"]] 1947 assignee: Any 1948 assignees: Any 1949 body: str 1950 closed_at: Any 1951 comments: int 1952 created_at: str 1953 due_date: Any 1954 html_url: str 1955 id: int 1956 is_locked: bool 1957 labels: List[Any] 1958 milestone: Optional["Milestone"] 1959 number: int 1960 original_author: str 1961 original_author_id: int 1962 pin_order: int 1963 pull_request: Any 1964 ref: str 1965 repository: Dict[str, Union[int, str]] 1966 state: str 1967 title: str 1968 updated_at: str 1969 url: str 1970 user: User 1971 1972 API_OBJECT = """/repos/{owner}/{repo}/issues/{index}""" # <owner, repo, index> 1973 GET_TIME = """/repos/%s/%s/issues/%s/times""" # <owner, repo, index> 1974 GET_COMMENTS = """/repos/{owner}/{repo}/issues/{index}/comments""" 1975 CREATE_ISSUE = """/repos/{owner}/{repo}/issues""" 1976 GET_ATTACHMENTS = """/repos/{owner}/{repo}/issues/{index}/assets""" 1977 1978 OPENED = "open" 1979 CLOSED = "closed" 1980 1981 def __init__(self, allspice_client): 1982 super().__init__(allspice_client) 1983 1984 def __eq__(self, other): 1985 if not isinstance(other, Issue): 1986 return False 1987 return self.repository == other.repository and self.id == other.id 1988 1989 def __hash__(self): 1990 return hash(self.repository) ^ hash(self.id) 1991 1992 _fields_to_parsers: ClassVar[dict] = { 1993 "milestone": lambda allspice_client, m: Milestone.parse_response(allspice_client, m), 1994 "user": lambda allspice_client, u: User.parse_response(allspice_client, u), 1995 "assets": lambda allspice_client, assets: [ 1996 Attachment.parse_response(allspice_client, a) for a in assets 1997 ], 1998 "assignee": lambda allspice_client, u: User.parse_response(allspice_client, u), 1999 "assignees": lambda allspice_client, us: [ 2000 User.parse_response(allspice_client, u) for u in us 2001 ], 2002 "state": lambda _, s: Issue.CLOSED if s == "closed" else Issue.OPENED, 2003 } 2004 2005 _parsers_to_fields: ClassVar[dict] = { 2006 "milestone": lambda m: m.id, 2007 } 2008 2009 _patchable_fields: ClassVar[set[str]] = { 2010 "assignee", 2011 "assignees", 2012 "body", 2013 "due_date", 2014 "milestone", 2015 "state", 2016 "title", 2017 } 2018 2019 def commit(self): 2020 args = { 2021 "owner": self.repository.owner.username, 2022 "repo": self.repository.name, 2023 "index": self.number, 2024 } 2025 self._commit(args) 2026 2027 @classmethod 2028 def request(cls, allspice_client, owner: str, repo: str, number: str): 2029 api_object = cls._request(allspice_client, {"owner": owner, "repo": repo, "index": number}) 2030 # The repository in the response is a RepositoryMeta object, so request 2031 # the full repository object and add it to the issue object. 2032 repository = Repository.request(allspice_client, owner, repo) 2033 setattr(api_object, "_repository", repository) 2034 # For legacy reasons 2035 cls._add_read_property("repo", repository, api_object) 2036 return api_object 2037 2038 @classmethod 2039 def create_issue(cls, allspice_client, repo: Repository, title: str, body: str = ""): 2040 args = {"owner": repo.owner.username, "repo": repo.name} 2041 data = {"title": title, "body": body} 2042 result = allspice_client.requests_post(Issue.CREATE_ISSUE.format(**args), data=data) 2043 issue = Issue.parse_response(allspice_client, result) 2044 setattr(issue, "_repository", repo) 2045 cls._add_read_property("repo", repo, issue) 2046 return issue 2047 2048 @property 2049 def owner(self) -> Organization | User: 2050 return self.repository.owner 2051 2052 def get_time_sum(self, user: User) -> int: 2053 results = self.allspice_client.requests_get( 2054 Issue.GET_TIME % (self.owner.username, self.repository.name, self.number) 2055 ) 2056 return sum(result["time"] for result in results if result and result["user_id"] == user.id) 2057 2058 def get_times(self) -> Optional[Dict]: 2059 return self.allspice_client.requests_get( 2060 Issue.GET_TIME % (self.owner.username, self.repository.name, self.number) 2061 ) 2062 2063 def delete_time(self, time_id: str): 2064 path = f"/repos/{self.owner.username}/{self.repository.name}/issues/{self.number}/times/{time_id}" 2065 self.allspice_client.requests_delete(path) 2066 2067 def add_time(self, time: int, created: Optional[str] = None, user_name: Optional[User] = None): 2068 path = f"/repos/{self.owner.username}/{self.repository.name}/issues/{self.number}/times" 2069 self.allspice_client.requests_post( 2070 path, data={"created": created, "time": int(time), "user_name": user_name} 2071 ) 2072 2073 def get_comments(self) -> List[Comment]: 2074 """https://hub.allspice.io/api/swagger#/issue/issueGetComments""" 2075 2076 results = self.allspice_client.requests_get( 2077 self.GET_COMMENTS.format( 2078 owner=self.owner.username, repo=self.repository.name, index=self.number 2079 ) 2080 ) 2081 2082 return [Comment.parse_response(self.allspice_client, result) for result in results] 2083 2084 def create_comment(self, body: str) -> Comment: 2085 """https://hub.allspice.io/api/swagger#/issue/issueCreateComment""" 2086 2087 path = self.GET_COMMENTS.format( 2088 owner=self.owner.username, repo=self.repository.name, index=self.number 2089 ) 2090 2091 response = self.allspice_client.requests_post(path, data={"body": body}) 2092 return Comment.parse_response(self.allspice_client, response) 2093 2094 def get_attachments(self) -> List[Attachment]: 2095 """ 2096 Fetch all attachments on this issue. 2097 2098 Unlike the assets field, this will always fetch all attachments from the 2099 API. 2100 2101 See https://hub.allspice.io/api/swagger#/issue/issueListIssueAttachments 2102 """ 2103 2104 path = self.GET_ATTACHMENTS.format( 2105 owner=self.owner.username, repo=self.repository.name, index=self.number 2106 ) 2107 response = self.allspice_client.requests_get(path) 2108 2109 return [Attachment.parse_response(self.allspice_client, result) for result in response] 2110 2111 def create_attachment(self, file: IO, name: Optional[str] = None) -> Attachment: 2112 """ 2113 Create an attachment on this issue. 2114 2115 https://hub.allspice.io/api/swagger#/issue/issueCreateIssueAttachment 2116 2117 :param file: The file to attach. This should be a file-like object. 2118 :param name: The name of the file. If not provided, the name of the file will be 2119 used. 2120 :return: The created attachment. 2121 """ 2122 2123 args: dict[str, Any] = { 2124 "files": {"attachment": file}, 2125 } 2126 if name is not None: 2127 args["params"] = {"name": name} 2128 2129 result = self.allspice_client.requests_post( 2130 self.GET_ATTACHMENTS.format( 2131 owner=self.owner.username, repo=self.repository.name, index=self.number 2132 ), 2133 **args, 2134 ) 2135 2136 return Attachment.parse_response(self.allspice_client, result)
An issue on a repository.
Note: Issue.assets may not have any entries even if the issue has
attachments. This happens when an issue is fetched via a bulk method like
Repository.get_issues. In most cases, prefer using
Issue.get_attachments to get the attachments on an issue.
2027 @classmethod 2028 def request(cls, allspice_client, owner: str, repo: str, number: str): 2029 api_object = cls._request(allspice_client, {"owner": owner, "repo": repo, "index": number}) 2030 # The repository in the response is a RepositoryMeta object, so request 2031 # the full repository object and add it to the issue object. 2032 repository = Repository.request(allspice_client, owner, repo) 2033 setattr(api_object, "_repository", repository) 2034 # For legacy reasons 2035 cls._add_read_property("repo", repository, api_object) 2036 return api_object
2038 @classmethod 2039 def create_issue(cls, allspice_client, repo: Repository, title: str, body: str = ""): 2040 args = {"owner": repo.owner.username, "repo": repo.name} 2041 data = {"title": title, "body": body} 2042 result = allspice_client.requests_post(Issue.CREATE_ISSUE.format(**args), data=data) 2043 issue = Issue.parse_response(allspice_client, result) 2044 setattr(issue, "_repository", repo) 2045 cls._add_read_property("repo", repo, issue) 2046 return issue
2067 def add_time(self, time: int, created: Optional[str] = None, user_name: Optional[User] = None): 2068 path = f"/repos/{self.owner.username}/{self.repository.name}/issues/{self.number}/times" 2069 self.allspice_client.requests_post( 2070 path, data={"created": created, "time": int(time), "user_name": user_name} 2071 )
2073 def get_comments(self) -> List[Comment]: 2074 """https://hub.allspice.io/api/swagger#/issue/issueGetComments""" 2075 2076 results = self.allspice_client.requests_get( 2077 self.GET_COMMENTS.format( 2078 owner=self.owner.username, repo=self.repository.name, index=self.number 2079 ) 2080 ) 2081 2082 return [Comment.parse_response(self.allspice_client, result) for result in results]
allspice.allspice.io/api/swagger#/issue/issueGetComments">https://huballspice.allspice.io/api/swagger#/issue/issueGetComments
2084 def create_comment(self, body: str) -> Comment: 2085 """https://hub.allspice.io/api/swagger#/issue/issueCreateComment""" 2086 2087 path = self.GET_COMMENTS.format( 2088 owner=self.owner.username, repo=self.repository.name, index=self.number 2089 ) 2090 2091 response = self.allspice_client.requests_post(path, data={"body": body}) 2092 return Comment.parse_response(self.allspice_client, response)
allspice.allspice.io/api/swagger#/issue/issueCreateComment">https://huballspice.allspice.io/api/swagger#/issue/issueCreateComment
2094 def get_attachments(self) -> List[Attachment]: 2095 """ 2096 Fetch all attachments on this issue. 2097 2098 Unlike the assets field, this will always fetch all attachments from the 2099 API. 2100 2101 See https://hub.allspice.io/api/swagger#/issue/issueListIssueAttachments 2102 """ 2103 2104 path = self.GET_ATTACHMENTS.format( 2105 owner=self.owner.username, repo=self.repository.name, index=self.number 2106 ) 2107 response = self.allspice_client.requests_get(path) 2108 2109 return [Attachment.parse_response(self.allspice_client, result) for result in response]
Fetch all attachments on this issue.
Unlike the assets field, this will always fetch all attachments from the API.
See allspice.allspice.io/api/swagger#/issue/issueListIssueAttachments">https://huballspice.allspice.io/api/swagger#/issue/issueListIssueAttachments
2111 def create_attachment(self, file: IO, name: Optional[str] = None) -> Attachment: 2112 """ 2113 Create an attachment on this issue. 2114 2115 https://hub.allspice.io/api/swagger#/issue/issueCreateIssueAttachment 2116 2117 :param file: The file to attach. This should be a file-like object. 2118 :param name: The name of the file. If not provided, the name of the file will be 2119 used. 2120 :return: The created attachment. 2121 """ 2122 2123 args: dict[str, Any] = { 2124 "files": {"attachment": file}, 2125 } 2126 if name is not None: 2127 args["params"] = {"name": name} 2128 2129 result = self.allspice_client.requests_post( 2130 self.GET_ATTACHMENTS.format( 2131 owner=self.owner.username, repo=self.repository.name, index=self.number 2132 ), 2133 **args, 2134 ) 2135 2136 return Attachment.parse_response(self.allspice_client, result)
Create an attachment on this issue.
allspice.allspice.io/api/swagger#/issue/issueCreateIssueAttachment">https://huballspice.allspice.io/api/swagger#/issue/issueCreateIssueAttachment
Parameters
- file: The file to attach. This should be a file-like object.
- name: The name of the file. If not provided, the name of the file will be used.
Returns
The created attachment.
1507class Milestone(ApiObject): 1508 allow_merge_commits: Any 1509 allow_rebase: Any 1510 allow_rebase_explicit: Any 1511 allow_squash_merge: Any 1512 archived: Any 1513 closed_at: Any 1514 closed_issues: int 1515 created_at: str 1516 default_branch: Any 1517 description: str 1518 due_on: Any 1519 has_issues: Any 1520 has_pull_requests: Any 1521 has_wiki: Any 1522 id: int 1523 ignore_whitespace_conflicts: Any 1524 name: Any 1525 open_issues: int 1526 private: Any 1527 state: str 1528 title: str 1529 updated_at: str 1530 website: Any 1531 1532 API_OBJECT = """/repos/{owner}/{repo}/milestones/{number}""" # <owner, repo> 1533 1534 def __init__(self, allspice_client): 1535 super().__init__(allspice_client) 1536 1537 def __eq__(self, other): 1538 if not isinstance(other, Milestone): 1539 return False 1540 return self.allspice_client == other.allspice_client and self.id == other.id 1541 1542 def __hash__(self): 1543 return hash(self.allspice_client) ^ hash(self.id) 1544 1545 _fields_to_parsers: ClassVar[dict] = { 1546 "closed_at": lambda _, t: Util.convert_time(t), 1547 "due_on": lambda _, t: Util.convert_time(t), 1548 } 1549 1550 _patchable_fields: ClassVar[set[str]] = { 1551 "allow_merge_commits", 1552 "allow_rebase", 1553 "allow_rebase_explicit", 1554 "allow_squash_merge", 1555 "archived", 1556 "default_branch", 1557 "description", 1558 "has_issues", 1559 "has_pull_requests", 1560 "has_wiki", 1561 "ignore_whitespace_conflicts", 1562 "name", 1563 "private", 1564 "website", 1565 } 1566 1567 @classmethod 1568 def request(cls, allspice_client, owner: str, repo: str, number: str): 1569 return cls._request(allspice_client, {"owner": owner, "repo": repo, "number": number})
Common base class for all non-exit exceptions.
37class Organization(ApiObject): 38 """see https://hub.allspice.io/api/swagger#/organization/orgGetAll""" 39 40 active: Optional[bool] 41 avatar_url: str 42 created: Optional[str] 43 description: str 44 email: str 45 followers_count: Optional[int] 46 following_count: Optional[int] 47 full_name: str 48 html_url: Optional[str] 49 id: int 50 is_admin: Optional[bool] 51 language: Optional[str] 52 last_login: Optional[str] 53 location: str 54 login: Optional[str] 55 login_name: Optional[str] 56 name: Optional[str] 57 prohibit_login: Optional[bool] 58 repo_admin_change_team_access: Optional[bool] 59 restricted: Optional[bool] 60 source_id: Optional[int] 61 starred_repos_count: Optional[int] 62 username: str 63 visibility: str 64 website: str 65 66 API_OBJECT = """/orgs/{name}""" # <org> 67 ORG_REPOS_REQUEST = """/orgs/%s/repos""" # <org> 68 ORG_TEAMS_REQUEST = """/orgs/%s/teams""" # <org> 69 ORG_TEAMS_CREATE = """/orgs/%s/teams""" # <org> 70 ORG_GET_MEMBERS = """/orgs/%s/members""" # <org> 71 ORG_IS_MEMBER = """/orgs/%s/members/%s""" # <org>, <username> 72 ORG_HEATMAP = """/users/%s/heatmap""" # <username> 73 74 def __init__(self, allspice_client): 75 super().__init__(allspice_client) 76 77 def __eq__(self, other): 78 if not isinstance(other, Organization): 79 return False 80 return self.allspice_client == other.allspice_client and self.name == other.name 81 82 def __hash__(self): 83 return hash(self.allspice_client) ^ hash(self.name) 84 85 @classmethod 86 def request(cls, allspice_client, name: str) -> Self: 87 return cls._request(allspice_client, {"name": name}) 88 89 @classmethod 90 def parse_response(cls, allspice_client, result) -> "Organization": 91 api_object = super().parse_response(allspice_client, result) 92 # add "name" field to make this behave similar to users for gitea < 1.18 93 # also necessary for repository-owner when org is repo owner 94 if not hasattr(api_object, "name"): 95 Organization._add_read_property("name", result["username"], api_object) 96 return api_object 97 98 _patchable_fields: ClassVar[set[str]] = { 99 "description", 100 "full_name", 101 "location", 102 "visibility", 103 "website", 104 } 105 106 def commit(self): 107 args = {"name": self.name} 108 self._commit(args) 109 110 def create_repo( 111 self, 112 repoName: str, 113 description: str = "", 114 private: bool = False, 115 autoInit=True, 116 gitignores: Optional[str] = None, 117 license: Optional[str] = None, 118 readme: str = "Default", 119 issue_labels: Optional[str] = None, 120 default_branch="master", 121 ): 122 """Create an organization Repository 123 124 Throws: 125 AlreadyExistsException: If the Repository exists already. 126 Exception: If something else went wrong. 127 """ 128 result = self.allspice_client.requests_post( 129 f"/orgs/{self.name}/repos", 130 data={ 131 "name": repoName, 132 "description": description, 133 "private": private, 134 "auto_init": autoInit, 135 "gitignores": gitignores, 136 "license": license, 137 "issue_labels": issue_labels, 138 "readme": readme, 139 "default_branch": default_branch, 140 }, 141 ) 142 if "id" in result: 143 self.allspice_client.logger.info("Successfully created Repository %s " % result["name"]) 144 else: 145 self.allspice_client.logger.error(result["message"]) 146 raise Exception("Repository not created... (gitea: %s)" % result["message"]) 147 return Repository.parse_response(self.allspice_client, result) 148 149 def get_repositories(self) -> List["Repository"]: 150 results = self.allspice_client.requests_get_paginated( 151 Organization.ORG_REPOS_REQUEST % self.username 152 ) 153 return [Repository.parse_response(self.allspice_client, result) for result in results] 154 155 def get_repository(self, name) -> "Repository": 156 repos = self.get_repositories() 157 for repo in repos: 158 if repo.name == name: 159 return repo 160 raise NotFoundException("Repository %s not existent in organization." % name) 161 162 def get_teams(self) -> List["Team"]: 163 results = self.allspice_client.requests_get(Organization.ORG_TEAMS_REQUEST % self.username) 164 teams = [Team.parse_response(self.allspice_client, result) for result in results] 165 # organisation seems to be missing using this request, so we add org manually 166 for t in teams: 167 setattr(t, "_organization", self) 168 return teams 169 170 def get_team(self, name) -> "Team": 171 teams = self.get_teams() 172 for team in teams: 173 if team.name == name: 174 return team 175 raise NotFoundException("Team not existent in organization.") 176 177 def create_team( 178 self, 179 name: str, 180 description: str = "", 181 permission: str = "read", 182 can_create_org_repo: bool = False, 183 includes_all_repositories: bool = False, 184 units=( 185 "repo.code", 186 "repo.issues", 187 "repo.ext_issues", 188 "repo.wiki", 189 "repo.pulls", 190 "repo.releases", 191 "repo.ext_wiki", 192 ), 193 units_map={}, 194 ) -> "Team": 195 """Alias for AllSpice#create_team""" 196 # TODO: Move AllSpice#create_team to Organization#create_team and 197 # deprecate AllSpice#create_team. 198 return self.allspice_client.create_team( 199 org=self, 200 name=name, 201 description=description, 202 permission=permission, 203 can_create_org_repo=can_create_org_repo, 204 includes_all_repositories=includes_all_repositories, 205 units=units, 206 units_map=units_map, 207 ) 208 209 def get_members(self) -> List["User"]: 210 results = self.allspice_client.requests_get(Organization.ORG_GET_MEMBERS % self.username) 211 return [User.parse_response(self.allspice_client, result) for result in results] 212 213 def is_member(self, username) -> bool: 214 if isinstance(username, User): 215 username = username.username 216 try: 217 # returns 204 if its ok, 404 if its not 218 self.allspice_client.requests_get( 219 Organization.ORG_IS_MEMBER % (self.username, username) 220 ) 221 return True 222 except Exception: 223 return False 224 225 def remove_member(self, user: "User"): 226 path = f"/orgs/{self.username}/members/{user.username}" 227 self.allspice_client.requests_delete(path) 228 229 def delete(self): 230 """Delete this Organization. Invalidates this Objects data. Also deletes all Repositories owned by the User""" 231 for repo in self.get_repositories(): 232 repo.delete() 233 self.allspice_client.requests_delete(Organization.API_OBJECT.format(name=self.username)) 234 self.deleted = True 235 236 def get_heatmap(self) -> List[Tuple[datetime, int]]: 237 results = self.allspice_client.requests_get(User.USER_HEATMAP % self.username) 238 results = [ 239 (datetime.fromtimestamp(result["timestamp"]), result["contributions"]) 240 for result in results 241 ] 242 return results
see allspice.allspice.io/api/swagger#/organization/orgGetAll">https://huballspice.allspice.io/api/swagger#/organization/orgGetAll
89 @classmethod 90 def parse_response(cls, allspice_client, result) -> "Organization": 91 api_object = super().parse_response(allspice_client, result) 92 # add "name" field to make this behave similar to users for gitea < 1.18 93 # also necessary for repository-owner when org is repo owner 94 if not hasattr(api_object, "name"): 95 Organization._add_read_property("name", result["username"], api_object) 96 return api_object
110 def create_repo( 111 self, 112 repoName: str, 113 description: str = "", 114 private: bool = False, 115 autoInit=True, 116 gitignores: Optional[str] = None, 117 license: Optional[str] = None, 118 readme: str = "Default", 119 issue_labels: Optional[str] = None, 120 default_branch="master", 121 ): 122 """Create an organization Repository 123 124 Throws: 125 AlreadyExistsException: If the Repository exists already. 126 Exception: If something else went wrong. 127 """ 128 result = self.allspice_client.requests_post( 129 f"/orgs/{self.name}/repos", 130 data={ 131 "name": repoName, 132 "description": description, 133 "private": private, 134 "auto_init": autoInit, 135 "gitignores": gitignores, 136 "license": license, 137 "issue_labels": issue_labels, 138 "readme": readme, 139 "default_branch": default_branch, 140 }, 141 ) 142 if "id" in result: 143 self.allspice_client.logger.info("Successfully created Repository %s " % result["name"]) 144 else: 145 self.allspice_client.logger.error(result["message"]) 146 raise Exception("Repository not created... (gitea: %s)" % result["message"]) 147 return Repository.parse_response(self.allspice_client, result)
Create an organization Repository
Throws: AlreadyExistsException: If the Repository exists already. Exception: If something else went wrong.
162 def get_teams(self) -> List["Team"]: 163 results = self.allspice_client.requests_get(Organization.ORG_TEAMS_REQUEST % self.username) 164 teams = [Team.parse_response(self.allspice_client, result) for result in results] 165 # organisation seems to be missing using this request, so we add org manually 166 for t in teams: 167 setattr(t, "_organization", self) 168 return teams
177 def create_team( 178 self, 179 name: str, 180 description: str = "", 181 permission: str = "read", 182 can_create_org_repo: bool = False, 183 includes_all_repositories: bool = False, 184 units=( 185 "repo.code", 186 "repo.issues", 187 "repo.ext_issues", 188 "repo.wiki", 189 "repo.pulls", 190 "repo.releases", 191 "repo.ext_wiki", 192 ), 193 units_map={}, 194 ) -> "Team": 195 """Alias for AllSpice#create_team""" 196 # TODO: Move AllSpice#create_team to Organization#create_team and 197 # deprecate AllSpice#create_team. 198 return self.allspice_client.create_team( 199 org=self, 200 name=name, 201 description=description, 202 permission=permission, 203 can_create_org_repo=can_create_org_repo, 204 includes_all_repositories=includes_all_repositories, 205 units=units, 206 units_map=units_map, 207 )
Alias for AllSpice#create_team
213 def is_member(self, username) -> bool: 214 if isinstance(username, User): 215 username = username.username 216 try: 217 # returns 204 if its ok, 404 if its not 218 self.allspice_client.requests_get( 219 Organization.ORG_IS_MEMBER % (self.username, username) 220 ) 221 return True 222 except Exception: 223 return False
229 def delete(self): 230 """Delete this Organization. Invalidates this Objects data. Also deletes all Repositories owned by the User""" 231 for repo in self.get_repositories(): 232 repo.delete() 233 self.allspice_client.requests_delete(Organization.API_OBJECT.format(name=self.username)) 234 self.deleted = True
Delete this Organization. Invalidates this Objects data. Also deletes all Repositories owned by the User
2703class Release(ApiObject): 2704 """ 2705 A release on a repo. 2706 """ 2707 2708 assets: List[Union[Any, Dict[str, Union[int, str]], "ReleaseAsset"]] 2709 author: User 2710 body: str 2711 created_at: str 2712 draft: bool 2713 html_url: str 2714 id: int 2715 name: str 2716 prerelease: bool 2717 published_at: str 2718 repo: Optional["Repository"] 2719 repository: Optional["Repository"] 2720 tag_name: str 2721 tarball_url: str 2722 target_commitish: str 2723 upload_url: str 2724 url: str 2725 zipball_url: str 2726 2727 API_OBJECT = "/repos/{owner}/{repo}/releases/{id}" 2728 RELEASE_CREATE_ASSET = "/repos/{owner}/{repo}/releases/{id}/assets" 2729 # Note that we don't strictly need the get_assets route, as the release 2730 # object already contains the assets. 2731 2732 def __init__(self, allspice_client): 2733 super().__init__(allspice_client) 2734 2735 def __eq__(self, other): 2736 if not isinstance(other, Release): 2737 return False 2738 return self.repo == other.repo and self.id == other.id 2739 2740 def __hash__(self): 2741 return hash(self.repo) ^ hash(self.id) 2742 2743 _fields_to_parsers: ClassVar[dict] = { 2744 "author": lambda allspice_client, author: User.parse_response(allspice_client, author), 2745 } 2746 _patchable_fields: ClassVar[set[str]] = { 2747 "body", 2748 "draft", 2749 "name", 2750 "prerelease", 2751 "tag_name", 2752 "target_commitish", 2753 } 2754 2755 @classmethod 2756 def parse_response(cls, allspice_client, result, repo) -> Release: 2757 release = super().parse_response(allspice_client, result) 2758 Release._add_read_property("repository", repo, release) 2759 # For legacy reasons 2760 Release._add_read_property("repo", repo, release) 2761 setattr( 2762 release, 2763 "_assets", 2764 [ 2765 ReleaseAsset.parse_response(allspice_client, asset, release) 2766 for asset in result["assets"] 2767 ], 2768 ) 2769 return release 2770 2771 @classmethod 2772 def request( 2773 cls, 2774 allspice_client, 2775 owner: str, 2776 repo: str, 2777 id: Optional[int] = None, 2778 ) -> Release: 2779 args = {"owner": owner, "repo": repo, "id": id} 2780 release_response = cls._get_gitea_api_object(allspice_client, args) 2781 repository = Repository.request(allspice_client, owner, repo) 2782 release = cls.parse_response(allspice_client, release_response, repository) 2783 return release 2784 2785 def commit(self): 2786 if self.repo is None: 2787 raise ValueError("Cannot commit a release without a repository.") 2788 2789 args = {"owner": self.repo.owner.username, "repo": self.repo.name, "id": self.id} 2790 self._commit(args) 2791 2792 def create_asset(self, file: IO, name: Optional[str] = None) -> ReleaseAsset: 2793 """ 2794 Create an asset for this release. 2795 2796 https://hub.allspice.io/api/swagger#/repository/repoCreateReleaseAsset 2797 2798 :param file: The file to upload. This should be a file-like object. 2799 :param name: The name of the file. 2800 :return: The created asset. 2801 """ 2802 2803 if self.repo is None: 2804 raise ValueError("Cannot commit a release without a repository.") 2805 2806 args: dict[str, Any] = {"files": {"attachment": file}} 2807 if name is not None: 2808 args["params"] = {"name": name} 2809 2810 result = self.allspice_client.requests_post( 2811 self.RELEASE_CREATE_ASSET.format( 2812 owner=self.repo.owner.username, 2813 repo=self.repo.name, 2814 id=self.id, 2815 ), 2816 **args, 2817 ) 2818 return ReleaseAsset.parse_response(self.allspice_client, result, self) 2819 2820 def delete(self): 2821 if self.repo is None: 2822 raise ValueError("Cannot commit a release without a repository.") 2823 2824 args = {"owner": self.repo.owner.username, "repo": self.repo.name, "id": self.id} 2825 self.allspice_client.requests_delete(self.API_OBJECT.format(**args)) 2826 self.deleted = True
A release on a repo.
2755 @classmethod 2756 def parse_response(cls, allspice_client, result, repo) -> Release: 2757 release = super().parse_response(allspice_client, result) 2758 Release._add_read_property("repository", repo, release) 2759 # For legacy reasons 2760 Release._add_read_property("repo", repo, release) 2761 setattr( 2762 release, 2763 "_assets", 2764 [ 2765 ReleaseAsset.parse_response(allspice_client, asset, release) 2766 for asset in result["assets"] 2767 ], 2768 ) 2769 return release
2771 @classmethod 2772 def request( 2773 cls, 2774 allspice_client, 2775 owner: str, 2776 repo: str, 2777 id: Optional[int] = None, 2778 ) -> Release: 2779 args = {"owner": owner, "repo": repo, "id": id} 2780 release_response = cls._get_gitea_api_object(allspice_client, args) 2781 repository = Repository.request(allspice_client, owner, repo) 2782 release = cls.parse_response(allspice_client, release_response, repository) 2783 return release
2792 def create_asset(self, file: IO, name: Optional[str] = None) -> ReleaseAsset: 2793 """ 2794 Create an asset for this release. 2795 2796 https://hub.allspice.io/api/swagger#/repository/repoCreateReleaseAsset 2797 2798 :param file: The file to upload. This should be a file-like object. 2799 :param name: The name of the file. 2800 :return: The created asset. 2801 """ 2802 2803 if self.repo is None: 2804 raise ValueError("Cannot commit a release without a repository.") 2805 2806 args: dict[str, Any] = {"files": {"attachment": file}} 2807 if name is not None: 2808 args["params"] = {"name": name} 2809 2810 result = self.allspice_client.requests_post( 2811 self.RELEASE_CREATE_ASSET.format( 2812 owner=self.repo.owner.username, 2813 repo=self.repo.name, 2814 id=self.id, 2815 ), 2816 **args, 2817 ) 2818 return ReleaseAsset.parse_response(self.allspice_client, result, self)
Create an asset for this release.
allspice.allspice.io/api/swagger#/repository/repoCreateReleaseAsset">https://huballspice.allspice.io/api/swagger#/repository/repoCreateReleaseAsset
Parameters
- file: The file to upload. This should be a file-like object.
- name: The name of the file.
Returns
The created asset.
2820 def delete(self): 2821 if self.repo is None: 2822 raise ValueError("Cannot commit a release without a repository.") 2823 2824 args = {"owner": self.repo.owner.username, "repo": self.repo.name, "id": self.id} 2825 self.allspice_client.requests_delete(self.API_OBJECT.format(**args)) 2826 self.deleted = True
41class RenderException(Exception): 42 """Raised when a server-side render fails; not retried.""" 43 44 @classmethod 45 def from_internal( 46 cls, internal: InternalServerException, file_path: str, ref: Optional[str] 47 ) -> Optional["RenderException"]: 48 body = internal.body 49 50 if body is None or body.category != "render": 51 return None 52 53 if body.message: 54 message = body.message 55 else: 56 message = f"Render failed for {file_path}" 57 if ref is not None: 58 message += f" at ref {ref}" 59 60 # Include a diagnostic url if present (a Hub link to the admin panel, don't include other links like swagger docs), 61 # otherwise just ask them to check Hub logs 62 if body.url and "-/admin/" in body.url: 63 message += f"; A diagnostic report for this failure can be downloaded by a site admin at {body.url}." 64 else: 65 message += "; Check AllSpice Hub logs for more information." 66 67 return cls(message)
Raised when a server-side render fails; not retried.
44 @classmethod 45 def from_internal( 46 cls, internal: InternalServerException, file_path: str, ref: Optional[str] 47 ) -> Optional["RenderException"]: 48 body = internal.body 49 50 if body is None or body.category != "render": 51 return None 52 53 if body.message: 54 message = body.message 55 else: 56 message = f"Render failed for {file_path}" 57 if ref is not None: 58 message += f" at ref {ref}" 59 60 # Include a diagnostic url if present (a Hub link to the admin panel, don't include other links like swagger docs), 61 # otherwise just ask them to check Hub logs 62 if body.url and "-/admin/" in body.url: 63 message += f"; A diagnostic report for this failure can be downloaded by a site admin at {body.url}." 64 else: 65 message += "; Check AllSpice Hub logs for more information." 66 67 return cls(message)
481class Repository(ApiObject): 482 allow_fast_forward_only_merge: bool 483 allow_manual_merge: Any 484 allow_merge_commits: bool 485 allow_rebase: bool 486 allow_rebase_explicit: bool 487 allow_rebase_update: bool 488 allow_squash_merge: bool 489 archived: bool 490 archived_at: str 491 autodetect_manual_merge: Any 492 avatar_url: str 493 clone_url: str 494 created_at: str 495 default_allow_maintainer_edit: bool 496 default_branch: str 497 default_delete_branch_after_merge: bool 498 default_merge_style: str 499 description: str 500 empty: bool 501 enable_prune: Any 502 external_tracker: Any 503 external_wiki: Any 504 fork: bool 505 forks_count: int 506 full_name: str 507 has_actions: bool 508 has_issues: bool 509 has_packages: bool 510 has_projects: bool 511 has_pull_requests: bool 512 has_releases: bool 513 has_wiki: bool 514 html_url: str 515 id: int 516 ignore_whitespace_conflicts: bool 517 internal: bool 518 internal_tracker: Dict[str, bool] 519 language: str 520 languages_url: str 521 licenses: List[Any] 522 link: str 523 mirror: bool 524 mirror_interval: str 525 mirror_updated: str 526 name: str 527 object_format_name: str 528 open_issues_count: int 529 open_pr_counter: int 530 original_url: str 531 owner: Union["User", "Organization"] 532 parent: Any 533 permissions: Dict[str, bool] 534 private: bool 535 projects_mode: str 536 release_counter: int 537 repo_transfer: Any 538 size: int 539 ssh_url: str 540 stars_count: int 541 template: bool 542 topics: List[Union[Any, str]] 543 updated_at: datetime 544 url: str 545 watchers_count: int 546 website: str 547 548 API_OBJECT = """/repos/{owner}/{name}""" # <owner>, <reponame> 549 REPO_IS_COLLABORATOR = """/repos/%s/%s/collaborators/%s""" # <owner>, <reponame>, <username> 550 REPO_SEARCH = """/repos/search/""" 551 REPO_BRANCHES = """/repos/%s/%s/branches""" # <owner>, <reponame> 552 REPO_BRANCH = """/repos/{owner}/{repo}/branches/{branch}""" 553 REPO_ISSUES = """/repos/{owner}/{repo}/issues""" # <owner, reponame> 554 REPO_DESIGN_REVIEWS = """/repos/{owner}/{repo}/pulls""" 555 REPO_DELETE = """/repos/%s/%s""" # <owner>, <reponame> 556 REPO_TIMES = """/repos/%s/%s/times""" # <owner>, <reponame> 557 REPO_USER_TIME = """/repos/%s/%s/times/%s""" # <owner>, <reponame>, <username> 558 REPO_COMMITS = "/repos/%s/%s/commits" # <owner>, <reponame> 559 REPO_TRANSFER = "/repos/{owner}/{repo}/transfer" 560 REPO_MILESTONES = """/repos/{owner}/{repo}/milestones""" 561 REPO_GET_ARCHIVE = "/repos/{owner}/{repo}/archive/{ref}.{format}" 562 REPO_GET_ALLSPICE_JSON = "/repos/{owner}/{repo}/allspice_generated/json/{content}" 563 REPO_GET_ALLSPICE_SVG = "/repos/{owner}/{repo}/allspice_generated/svg/{content}" 564 REPO_GET_ALLSPICE_PROJECT = "/repos/{owner}/{repo}/allspice_generated/project/{content}" 565 REPO_GET_TOPICS = "/repos/{owner}/{repo}/topics" 566 REPO_ADD_TOPIC = "/repos/{owner}/{repo}/topics/{topic}" 567 REPO_GET_RELEASES = "/repos/{owner}/{repo}/releases" 568 REPO_GET_LATEST_RELEASE = "/repos/{owner}/{repo}/releases/latest" 569 REPO_GET_RELEASE_BY_TAG = "/repos/{owner}/{repo}/releases/tags/{tag}" 570 REPO_GET_COMMIT_STATUS = "/repos/{owner}/{repo}/statuses/{sha}" 571 REPO_GET_MEDIA = "/repos/{owner}/{repo}/media/{path}" 572 REPO_GET_TREE = "/repos/{owner}/{repo}/git/trees/{ref}" 573 574 class ArchiveFormat(Enum): 575 """ 576 Archive formats for Repository.get_archive 577 """ 578 579 TAR = "tar.gz" 580 ZIP = "zip" 581 582 class CommitStatusSort(Enum): 583 """ 584 Sort order for Repository.get_commit_status 585 """ 586 587 OLDEST = "oldest" 588 RECENT_UPDATE = "recentupdate" 589 LEAST_UPDATE = "leastupdate" 590 LEAST_INDEX = "leastindex" 591 HIGHEST_INDEX = "highestindex" 592 593 def __init__(self, allspice_client): 594 super().__init__(allspice_client) 595 596 def __eq__(self, other): 597 if not isinstance(other, Repository): 598 return False 599 return self.owner == other.owner and self.name == other.name 600 601 def __hash__(self): 602 return hash(self.owner) ^ hash(self.name) 603 604 _fields_to_parsers: ClassVar[dict] = { 605 # dont know how to tell apart user and org as owner except form email being empty. 606 "owner": lambda allspice_client, r: ( 607 Organization.parse_response(allspice_client, r) 608 if r["email"] == "" 609 else User.parse_response(allspice_client, r) 610 ), 611 "updated_at": lambda _, t: Util.convert_time(t), 612 } 613 614 @classmethod 615 def request( 616 cls, 617 allspice_client, 618 owner: str, 619 name: str, 620 ) -> Repository: 621 return cls._request(allspice_client, {"owner": owner, "name": name}) 622 623 @classmethod 624 def search( 625 cls, 626 allspice_client, 627 query: Optional[str] = None, 628 topic: bool = False, 629 include_description: bool = False, 630 user: Optional[User] = None, 631 owner_to_prioritize: Union[User, Organization, None] = None, 632 ) -> list[Repository]: 633 """ 634 Search for repositories. 635 636 See https://hub.allspice.io/api/swagger#/repository/repoSearch 637 638 :param query: The query string to search for 639 :param topic: If true, the query string will only be matched against the 640 repository's topic. 641 :param include_description: If true, the query string will be matched 642 against the repository's description as well. 643 :param user: If specified, only repositories that this user owns or 644 contributes to will be searched. 645 :param owner_to_prioritize: If specified, repositories owned by the 646 given entity will be prioritized in the search. 647 :returns: All repositories matching the query. If there are many 648 repositories matching this query, this may take some time. 649 """ 650 651 params = {} 652 653 if query is not None: 654 params["q"] = query 655 if topic: 656 params["topic"] = topic 657 if include_description: 658 params["include_description"] = include_description 659 if user is not None: 660 params["user"] = user.id 661 if owner_to_prioritize is not None: 662 params["owner_to_prioritize"] = owner_to_prioritize.id 663 664 responses = allspice_client.requests_get_paginated(cls.REPO_SEARCH, params=params) 665 666 return [Repository.parse_response(allspice_client, response) for response in responses] 667 668 _patchable_fields: ClassVar[set[str]] = { 669 "allow_manual_merge", 670 "allow_merge_commits", 671 "allow_rebase", 672 "allow_rebase_explicit", 673 "allow_rebase_update", 674 "allow_squash_merge", 675 "archived", 676 "autodetect_manual_merge", 677 "default_branch", 678 "default_delete_branch_after_merge", 679 "default_merge_style", 680 "description", 681 "enable_prune", 682 "external_tracker", 683 "external_wiki", 684 "has_actions", 685 "has_issues", 686 "has_projects", 687 "has_pull_requests", 688 "has_wiki", 689 "ignore_whitespace_conflicts", 690 "internal_tracker", 691 "mirror_interval", 692 "name", 693 "private", 694 "template", 695 "website", 696 } 697 698 def commit(self): 699 args = {"owner": self.owner.username, "name": self.name} 700 self._commit(args) 701 702 def get_branches(self) -> List["Branch"]: 703 """Get all the Branches of this Repository.""" 704 705 results = self.allspice_client.requests_get_paginated( 706 Repository.REPO_BRANCHES % (self.owner.username, self.name) 707 ) 708 return [Branch.parse_response(self.allspice_client, result) for result in results] 709 710 def get_branch(self, name: str) -> "Branch": 711 """Get a specific Branch of this Repository.""" 712 result = self.allspice_client.requests_get( 713 Repository.REPO_BRANCH.format(owner=self.owner.username, repo=self.name, branch=name) 714 ) 715 return Branch.parse_response(self.allspice_client, result) 716 717 def add_branch(self, create_from: Ref, newname: str) -> "Branch": 718 """Add a branch to the repository""" 719 # Note: will only work with gitea 1.13 or higher! 720 721 ref_name = Util.data_params_for_ref(create_from) 722 if "ref" not in ref_name: 723 raise ValueError("create_from must be a Branch, Commit or string") 724 ref_name = ref_name["ref"] 725 726 data = {"new_branch_name": newname, "old_ref_name": ref_name} 727 result = self.allspice_client.requests_post( 728 Repository.REPO_BRANCHES % (self.owner.username, self.name), data=data 729 ) 730 return Branch.parse_response(self.allspice_client, result) 731 732 def get_issues( 733 self, 734 state: Literal["open", "closed", "all"] = "all", 735 search_query: Optional[str] = None, 736 labels: Optional[List[str]] = None, 737 milestones: Optional[List[Union[Milestone, str]]] = None, 738 assignee: Optional[Union[User, str]] = None, 739 since: Optional[datetime] = None, 740 before: Optional[datetime] = None, 741 ) -> List["Issue"]: 742 """ 743 Get all Issues of this Repository (open and closed) 744 745 https://hub.allspice.io/api/swagger#/repository/repoListIssues 746 747 All params of this method are optional filters. If you don't specify a filter, it 748 will not be applied. 749 750 :param state: The state of the Issues to get. If None, all Issues are returned. 751 :param search_query: Filter issues by text. This is equivalent to searching for 752 `search_query` in the Issues on the web interface. 753 :param labels: Filter issues by labels. 754 :param milestones: Filter issues by milestones. 755 :param assignee: Filter issues by the assigned user. 756 :param since: Filter issues by the date they were created. 757 :param before: Filter issues by the date they were created. 758 :return: A list of Issues. 759 """ 760 761 data = { 762 "state": state, 763 } 764 if search_query: 765 data["q"] = search_query 766 if labels: 767 data["labels"] = ",".join(labels) 768 if milestones: 769 data["milestone"] = ",".join( 770 [ 771 milestone.name if isinstance(milestone, Milestone) else milestone 772 for milestone in milestones 773 ] 774 ) 775 if assignee: 776 if isinstance(assignee, User): 777 data["assignee"] = assignee.username 778 else: 779 data["assignee"] = assignee 780 if since: 781 data["since"] = Util.format_time(since) 782 if before: 783 data["before"] = Util.format_time(before) 784 785 results = self.allspice_client.requests_get_paginated( 786 Repository.REPO_ISSUES.format(owner=self.owner.username, repo=self.name), 787 params=data, 788 ) 789 790 issues = [] 791 for result in results: 792 issue = Issue.parse_response(self.allspice_client, result) 793 # See Issue.request 794 setattr(issue, "_repository", self) 795 # This is mostly for compatibility with an older implementation 796 Issue._add_read_property("repo", self, issue) 797 issues.append(issue) 798 799 return issues 800 801 def get_design_reviews( 802 self, 803 state: Literal["open", "closed", "all"] = "all", 804 milestone: Optional[Union[Milestone, str]] = None, 805 labels: Optional[List[str]] = None, 806 ) -> List["DesignReview"]: 807 """ 808 Get all Design Reviews of this Repository. 809 810 https://hub.allspice.io/api/swagger#/repository/repoListPullRequests 811 812 :param state: The state of the Design Reviews to get. If None, all Design Reviews 813 are returned. 814 :param milestone: The milestone of the Design Reviews to get. 815 :param labels: A list of label IDs to filter DRs by. 816 :return: A list of Design Reviews. 817 """ 818 819 params = { 820 "state": state, 821 } 822 if milestone: 823 if isinstance(milestone, Milestone): 824 params["milestone"] = milestone.name 825 else: 826 params["milestone"] = milestone 827 if labels: 828 params["labels"] = ",".join(labels) 829 830 results = self.allspice_client.requests_get_paginated( 831 self.REPO_DESIGN_REVIEWS.format(owner=self.owner.username, repo=self.name), 832 params=params, 833 ) 834 return [DesignReview.parse_response(self.allspice_client, result) for result in results] 835 836 def get_commits( 837 self, 838 sha: Optional[str] = None, 839 path: Optional[str] = None, 840 stat: bool = True, 841 ) -> List["Commit"]: 842 """ 843 Get all the Commits of this Repository. 844 845 https://hub.allspice.io/api/swagger#/repository/repoGetAllCommits 846 847 :param sha: The SHA of the commit to start listing commits from. 848 :param path: filepath of a file/dir. 849 :param stat: Include the number of additions and deletions in the response. 850 Disable for speedup. 851 :return: A list of Commits. 852 """ 853 854 data = {} 855 if sha: 856 data["sha"] = sha 857 if path: 858 data["path"] = path 859 if not stat: 860 data["stat"] = False 861 862 try: 863 results = self.allspice_client.requests_get_paginated( 864 Repository.REPO_COMMITS % (self.owner.username, self.name), 865 params=data, 866 ) 867 except ConflictException as err: 868 logging.warning(err) 869 logging.warning("Repository %s/%s is Empty" % (self.owner.username, self.name)) 870 results = [] 871 return [Commit.parse_response(self.allspice_client, result) for result in results] 872 873 def get_issues_state(self, state) -> List["Issue"]: 874 """ 875 DEPRECATED: Use get_issues() instead. 876 877 Get issues of state Issue.open or Issue.closed of a repository. 878 """ 879 880 assert state in [Issue.OPENED, Issue.CLOSED] 881 issues = [] 882 data = {"state": state} 883 results = self.allspice_client.requests_get_paginated( 884 Repository.REPO_ISSUES.format(owner=self.owner.username, repo=self.name), 885 params=data, 886 ) 887 for result in results: 888 issue = Issue.parse_response(self.allspice_client, result) 889 # adding data not contained in the issue response 890 # See Issue.request() 891 setattr(issue, "_repository", self) 892 Issue._add_read_property("repo", self, issue) 893 Issue._add_read_property("owner", self.owner, issue) 894 issues.append(issue) 895 return issues 896 897 def get_times(self): 898 results = self.allspice_client.requests_get( 899 Repository.REPO_TIMES % (self.owner.username, self.name) 900 ) 901 return results 902 903 def get_user_time(self, username) -> float: 904 if isinstance(username, User): 905 username = username.username 906 results = self.allspice_client.requests_get( 907 Repository.REPO_USER_TIME % (self.owner.username, self.name, username) 908 ) 909 time = sum(r["time"] for r in results) 910 return time 911 912 def get_full_name(self) -> str: 913 return self.owner.username + "/" + self.name 914 915 def create_issue(self, title, assignees=frozenset(), description="") -> ApiObject: 916 data = { 917 "assignees": assignees, 918 "body": description, 919 "closed": False, 920 "title": title, 921 } 922 result = self.allspice_client.requests_post( 923 Repository.REPO_ISSUES.format(owner=self.owner.username, repo=self.name), 924 data=data, 925 ) 926 927 issue = Issue.parse_response(self.allspice_client, result) 928 setattr(issue, "_repository", self) 929 Issue._add_read_property("repo", self, issue) 930 return issue 931 932 def create_design_review( 933 self, 934 title: str, 935 head: Union[Branch, str], 936 base: Union[Branch, str], 937 assignees: Optional[Set[Union[User, str]]] = None, 938 body: Optional[str] = None, 939 due_date: Optional[datetime] = None, 940 milestone: Optional["Milestone"] = None, 941 ) -> "DesignReview": 942 """ 943 Create a new Design Review. 944 945 See https://hub.allspice.io/api/swagger#/repository/repoCreatePullRequest 946 947 :param title: Title of the Design Review 948 :param head: Branch or name of the branch to merge into the base branch 949 :param base: Branch or name of the branch to merge into 950 :param assignees: Optional. A list of users to assign this review. List can be of 951 User objects or of usernames. 952 :param body: An Optional Description for the Design Review. 953 :param due_date: An Optional Due date for the Design Review. 954 :param milestone: An Optional Milestone for the Design Review 955 :return: The created Design Review 956 """ 957 958 data: dict[str, Any] = { 959 "title": title, 960 } 961 962 if isinstance(head, Branch): 963 data["head"] = head.name 964 else: 965 data["head"] = head 966 if isinstance(base, Branch): 967 data["base"] = base.name 968 else: 969 data["base"] = base 970 if assignees: 971 data["assignees"] = [a.username if isinstance(a, User) else a for a in assignees] 972 if body: 973 data["body"] = body 974 if due_date: 975 data["due_date"] = Util.format_time(due_date) 976 if milestone: 977 data["milestone"] = milestone.id 978 979 result = self.allspice_client.requests_post( 980 self.REPO_DESIGN_REVIEWS.format(owner=self.owner.username, repo=self.name), 981 data=data, 982 ) 983 984 return DesignReview.parse_response(self.allspice_client, result) 985 986 def create_milestone( 987 self, 988 title: str, 989 description: str, 990 due_date: Optional[str] = None, 991 state: str = "open", 992 ) -> "Milestone": 993 url = Repository.REPO_MILESTONES.format(owner=self.owner.username, repo=self.name) 994 data = {"title": title, "description": description, "state": state} 995 if due_date: 996 data["due_date"] = due_date 997 result = self.allspice_client.requests_post(url, data=data) 998 return Milestone.parse_response(self.allspice_client, result) 999 1000 def create_gitea_hook(self, hook_url: str, events: List[str]): 1001 url = f"/repos/{self.owner.username}/{self.name}/hooks" 1002 data = { 1003 "type": "gitea", 1004 "config": {"content_type": "json", "url": hook_url}, 1005 "events": events, 1006 "active": True, 1007 } 1008 return self.allspice_client.requests_post(url, data=data) 1009 1010 def list_hooks(self): 1011 url = f"/repos/{self.owner.username}/{self.name}/hooks" 1012 return self.allspice_client.requests_get(url) 1013 1014 def delete_hook(self, id: str): 1015 url = f"/repos/{self.owner.username}/{self.name}/hooks/{id}" 1016 self.allspice_client.requests_delete(url) 1017 1018 def is_collaborator(self, username) -> bool: 1019 if isinstance(username, User): 1020 username = username.username 1021 try: 1022 # returns 204 if its ok, 404 if its not 1023 self.allspice_client.requests_get( 1024 Repository.REPO_IS_COLLABORATOR % (self.owner.username, self.name, username) 1025 ) 1026 return True 1027 except Exception: 1028 return False 1029 1030 def get_users_with_access(self) -> Sequence[User]: 1031 url = f"/repos/{self.owner.username}/{self.name}/collaborators" 1032 response = self.allspice_client.requests_get(url) 1033 collabs = [User.parse_response(self.allspice_client, user) for user in response] 1034 if isinstance(self.owner, User): 1035 return [*collabs, self.owner] 1036 else: 1037 # owner must be org 1038 teams = self.owner.get_teams() 1039 for team in teams: 1040 team_repos = team.get_repos() 1041 if self.name in [n.name for n in team_repos]: 1042 collabs += team.get_members() 1043 return collabs 1044 1045 def remove_collaborator(self, user_name: str): 1046 url = f"/repos/{self.owner.username}/{self.name}/collaborators/{user_name}" 1047 self.allspice_client.requests_delete(url) 1048 1049 def transfer_ownership( 1050 self, 1051 new_owner: Union[User, Organization], 1052 new_teams: Set[Team] | FrozenSet[Team] = frozenset(), 1053 ): 1054 url = Repository.REPO_TRANSFER.format(owner=self.owner.username, repo=self.name) 1055 data: dict[str, Any] = {"new_owner": new_owner.username} 1056 if isinstance(new_owner, Organization): 1057 new_team_ids = [team.id for team in new_teams if team in new_owner.get_teams()] 1058 data["team_ids"] = new_team_ids 1059 self.allspice_client.requests_post(url, data=data) 1060 # TODO: make sure this instance is either updated or discarded 1061 1062 def get_git_content( 1063 self, 1064 ref: Optional["Ref"] = None, 1065 commit: "Optional[Commit]" = None, 1066 ) -> List[Content]: 1067 """ 1068 Get the metadata for all files in the root directory. 1069 1070 https://hub.allspice.io/api/swagger#/repository/repoGetContentsList 1071 1072 :param ref: branch or commit to get content from 1073 :param commit: commit to get content from (deprecated) 1074 """ 1075 url = f"/repos/{self.owner.username}/{self.name}/contents" 1076 data = Util.data_params_for_ref(ref or commit) 1077 1078 result = [ 1079 Content.parse_response(self.allspice_client, f) 1080 for f in self.allspice_client.requests_get(url, data) 1081 ] 1082 return result 1083 1084 def get_tree(self, ref: Optional[Ref] = None, recursive: bool = False) -> List[GitEntry]: 1085 """ 1086 Get the repository's tree on a given ref. 1087 1088 By default, this will only return the top-level entries in the tree. If you want 1089 to get the entire tree, set `recursive` to True. 1090 1091 :param ref: The ref to get the tree from. If not provided, the default branch is used. 1092 :param recursive: Whether to get the entire tree or just the top-level entries. 1093 """ 1094 1095 ref = Util.data_params_for_ref(ref).get("ref", self.default_branch) 1096 url = self.REPO_GET_TREE.format(owner=self.owner.username, repo=self.name, ref=ref) 1097 params = {"recursive": recursive} 1098 results = self.allspice_client.requests_get_paginated(url, params=params) 1099 return [GitEntry.parse_response(self.allspice_client, result) for result in results] 1100 1101 def get_file_content( 1102 self, 1103 content: Content, 1104 ref: Optional[Ref] = None, 1105 commit: Optional[Commit] = None, 1106 ) -> Union[str, List["Content"]]: 1107 """https://hub.allspice.io/api/swagger#/repository/repoGetContents""" 1108 url = f"/repos/{self.owner.username}/{self.name}/contents/{content.path}" 1109 data = Util.data_params_for_ref(ref or commit) 1110 1111 if content.type == Content.FILE: 1112 return self.allspice_client.requests_get(url, data)["content"] 1113 else: 1114 return [ 1115 Content.parse_response(self.allspice_client, f) 1116 for f in self.allspice_client.requests_get(url, data) 1117 ] 1118 1119 def get_raw_file( 1120 self, 1121 file_path: str, 1122 ref: Optional[Ref] = None, 1123 ) -> bytes: 1124 """ 1125 Get the raw, binary data of a single file. 1126 1127 Note 1: if the file you are requesting is a text file, you might want to 1128 use .decode() on the result to get a string. For example: 1129 1130 content = repo.get_raw_file("file.txt").decode("utf-8") 1131 1132 Note 2: this method will store the entire file in memory. If you want 1133 to download a large file, you might want to use `download_to_file` 1134 instead. 1135 1136 See https://hub.allspice.io/api/swagger#/repository/repoGetRawFileOrLFS 1137 1138 :param file_path: The path to the file to get. 1139 :param ref: The branch or commit to get the file from. If not provided, 1140 the default branch is used. 1141 """ 1142 1143 url = self.REPO_GET_MEDIA.format( 1144 owner=self.owner.username, 1145 repo=self.name, 1146 path=file_path, 1147 ) 1148 params = Util.data_params_for_ref(ref) 1149 return self.allspice_client.requests_get_raw(url, params=params) 1150 1151 def download_to_file( 1152 self, 1153 file_path: str, 1154 io: IO, 1155 ref: Optional[Ref] = None, 1156 ) -> None: 1157 """ 1158 Download the binary data of a file to a file-like object. 1159 1160 Example: 1161 1162 with open("schematic.DSN", "wb") as f: 1163 Repository.download_to_file("Schematics/my_schematic.DSN", f) 1164 1165 :param file_path: The path to the file in the repository from the root 1166 of the repository. 1167 :param io: The file-like object to write the data to. 1168 """ 1169 1170 url = self.allspice_client._AllSpice__get_url( 1171 self.REPO_GET_MEDIA.format( 1172 owner=self.owner.username, 1173 repo=self.name, 1174 path=file_path, 1175 ) 1176 ) 1177 params = Util.data_params_for_ref(ref) 1178 response = self.allspice_client.requests.get( 1179 url, 1180 params=params, 1181 headers=self.allspice_client.headers, 1182 stream=True, 1183 ) 1184 1185 for chunk in response.iter_content(chunk_size=4096): 1186 if chunk: 1187 io.write(chunk) 1188 1189 def get_generated_json( 1190 self, 1191 content: Union[Content, str], 1192 ref: Optional[Ref] = None, 1193 params: Optional[dict] = None, 1194 ) -> dict: 1195 """ 1196 Get the json blob for a cad file if it exists, otherwise enqueue 1197 a new job and return a 503 status. 1198 1199 WARNING: This is still experimental and not recommended for critical 1200 applications. The structure and content of the returned dictionary can 1201 change at any time. 1202 1203 See https://hub.allspice.io/api/swagger#/repository/repoGetAllSpiceJSON 1204 """ 1205 1206 if isinstance(content, Content): 1207 content = content.path 1208 1209 url = self.REPO_GET_ALLSPICE_JSON.format( 1210 owner=self.owner.username, 1211 repo=self.name, 1212 content=content, 1213 ) 1214 data = Util.data_params_for_ref(ref) 1215 if params: 1216 data.update(params) 1217 if self.allspice_client.use_new_schdoc_renderer is not None: 1218 data["use_new_schdoc_renderer"] = ( 1219 "true" if self.allspice_client.use_new_schdoc_renderer else "false" 1220 ) 1221 return self.allspice_client.requests_get(url, data) 1222 1223 def get_generated_svg( 1224 self, 1225 content: Union[Content, str], 1226 ref: Optional[Ref] = None, 1227 params: Optional[dict] = None, 1228 ) -> bytes: 1229 """ 1230 Get the svg blob for a cad file if it exists, otherwise enqueue 1231 a new job and return a 503 status. 1232 1233 WARNING: This is still experimental and not yet recommended for 1234 critical applications. The content of the returned svg can change 1235 at any time. 1236 1237 See https://hub.allspice.io/api/swagger#/repository/repoGetAllSpiceSVG 1238 """ 1239 1240 if isinstance(content, Content): 1241 content = content.path 1242 1243 url = self.REPO_GET_ALLSPICE_SVG.format( 1244 owner=self.owner.username, 1245 repo=self.name, 1246 content=content, 1247 ) 1248 data = Util.data_params_for_ref(ref) 1249 if params: 1250 data.update(params) 1251 if self.allspice_client.use_new_schdoc_renderer is not None: 1252 data["use_new_schdoc_renderer"] = ( 1253 "true" if self.allspice_client.use_new_schdoc_renderer else "false" 1254 ) 1255 return self.allspice_client.requests_get_raw(url, data) 1256 1257 def get_generated_projectdata( 1258 self, 1259 content: Union[Content, str], 1260 ref: Optional[Ref] = None, 1261 params: Optional[dict] = None, 1262 ) -> dict: 1263 """ 1264 Get the json project data based on the cad file provided 1265 1266 WARNING: This is still experimental and not yet recommended for 1267 critical applications. The content of the returned dictionary can change 1268 at any time. 1269 1270 See https://hub.allspice.io/api/swagger#/repository/repoGetAllSpiceProject 1271 """ 1272 if isinstance(content, Content): 1273 content = content.path 1274 1275 url = self.REPO_GET_ALLSPICE_PROJECT.format( 1276 owner=self.owner.username, 1277 repo=self.name, 1278 content=content, 1279 ) 1280 data = Util.data_params_for_ref(ref) 1281 if params: 1282 data.update(params) 1283 return self.allspice_client.requests_get(url, data) 1284 1285 def create_file(self, file_path: str, content: str, data: Optional[dict] = None): 1286 """https://hub.allspice.io/api/swagger#/repository/repoCreateFile""" 1287 if not data: 1288 data = {} 1289 url = f"/repos/{self.owner.username}/{self.name}/contents/{file_path}" 1290 data.update({"content": content}) 1291 return self.allspice_client.requests_post(url, data) 1292 1293 def change_file(self, file_path: str, file_sha: str, content: str, data: Optional[dict] = None): 1294 """https://hub.allspice.io/api/swagger#/repository/repoCreateFile""" 1295 if not data: 1296 data = {} 1297 url = f"/repos/{self.owner.username}/{self.name}/contents/{file_path}" 1298 data.update({"sha": file_sha, "content": content}) 1299 return self.allspice_client.requests_put(url, data) 1300 1301 def delete_file(self, file_path: str, file_sha: str, data: Optional[dict] = None): 1302 """https://hub.allspice.io/api/swagger#/repository/repoDeleteFile""" 1303 if not data: 1304 data = {} 1305 url = f"/repos/{self.owner.username}/{self.name}/contents/{file_path}" 1306 data.update({"sha": file_sha}) 1307 return self.allspice_client.requests_delete(url, data) 1308 1309 def get_archive( 1310 self, 1311 ref: Ref = "main", 1312 archive_format: ArchiveFormat = ArchiveFormat.ZIP, 1313 ) -> bytes: 1314 """ 1315 Download all the files in a specific ref of a repository as a zip or tarball 1316 archive. 1317 1318 https://hub.allspice.io/api/swagger#/repository/repoGetArchive 1319 1320 :param ref: branch or commit to get content from, defaults to the "main" branch 1321 :param archive_format: zip or tar, defaults to zip 1322 """ 1323 1324 ref_string = Util.data_params_for_ref(ref)["ref"] 1325 url = self.REPO_GET_ARCHIVE.format( 1326 owner=self.owner.username, 1327 repo=self.name, 1328 ref=ref_string, 1329 format=archive_format.value, 1330 ) 1331 return self.allspice_client.requests_get_raw(url) 1332 1333 def get_topics(self) -> list[str]: 1334 """ 1335 Gets the list of topics on this repository. 1336 1337 See http://localhost:3000/api/swagger#/repository/repoListTopics 1338 """ 1339 1340 url = self.REPO_GET_TOPICS.format( 1341 owner=self.owner.username, 1342 repo=self.name, 1343 ) 1344 return self.allspice_client.requests_get(url)["topics"] 1345 1346 def add_topic(self, topic: str): 1347 """ 1348 Adds a topic to the repository. 1349 1350 See https://hub.allspice.io/api/swagger#/repository/repoAddTopic 1351 1352 :param topic: The topic to add. Topic names must consist only of 1353 lowercase letters, numnbers and dashes (-), and cannot start with 1354 dashes. Topic names also must be under 35 characters long. 1355 """ 1356 1357 url = self.REPO_ADD_TOPIC.format(owner=self.owner.username, repo=self.name, topic=topic) 1358 self.allspice_client.requests_put(url) 1359 1360 def create_release( 1361 self, 1362 tag_name: str, 1363 name: Optional[str] = None, 1364 body: Optional[str] = None, 1365 draft: bool = False, 1366 ): 1367 """ 1368 Create a release for this repository. The release will be created for 1369 the tag with the given name. If there is no tag with this name, create 1370 the tag first. 1371 1372 See https://hub.allspice.io/api/swagger#/repository/repoCreateRelease 1373 """ 1374 1375 url = self.REPO_GET_RELEASES.format(owner=self.owner.username, repo=self.name) 1376 data = { 1377 "tag_name": tag_name, 1378 "draft": draft, 1379 } 1380 if name is not None: 1381 data["name"] = name 1382 if body is not None: 1383 data["body"] = body 1384 response = self.allspice_client.requests_post(url, data) 1385 return Release.parse_response(self.allspice_client, response, self) 1386 1387 def get_releases( 1388 self, draft: Optional[bool] = None, pre_release: Optional[bool] = None 1389 ) -> List[Release]: 1390 """ 1391 Get the list of releases for this repository. 1392 1393 See https://hub.allspice.io/api/swagger#/repository/repoListReleases 1394 """ 1395 1396 data = {} 1397 1398 if draft is not None: 1399 data["draft"] = draft 1400 if pre_release is not None: 1401 data["pre-release"] = pre_release 1402 1403 url = self.REPO_GET_RELEASES.format(owner=self.owner.username, repo=self.name) 1404 responses = self.allspice_client.requests_get_paginated(url, params=data) 1405 1406 return [ 1407 Release.parse_response(self.allspice_client, response, self) for response in responses 1408 ] 1409 1410 def get_latest_release(self) -> Release: 1411 """ 1412 Get the latest release for this repository. 1413 1414 See https://hub.allspice.io/api/swagger#/repository/repoGetLatestRelease 1415 """ 1416 1417 url = self.REPO_GET_LATEST_RELEASE.format(owner=self.owner.username, repo=self.name) 1418 response = self.allspice_client.requests_get(url) 1419 release = Release.parse_response(self.allspice_client, response, self) 1420 return release 1421 1422 def get_release_by_tag(self, tag: str) -> Release: 1423 """ 1424 Get a release by its tag. 1425 1426 See https://hub.allspice.io/api/swagger#/repository/repoGetReleaseByTag 1427 """ 1428 1429 url = self.REPO_GET_RELEASE_BY_TAG.format( 1430 owner=self.owner.username, repo=self.name, tag=tag 1431 ) 1432 response = self.allspice_client.requests_get(url) 1433 release = Release.parse_response(self.allspice_client, response, self) 1434 return release 1435 1436 def get_commit_statuses( 1437 self, 1438 commit: Union[str, Commit], 1439 sort: Optional[CommitStatusSort] = None, 1440 state: Optional[CommitStatusState] = None, 1441 ) -> List[CommitStatus]: 1442 """ 1443 Get a list of statuses for a commit. 1444 1445 This is roughly equivalent to the Commit.get_statuses method, but this 1446 method allows you to sort and filter commits and is more convenient if 1447 you have a commit SHA and don't need to get the commit itself. 1448 1449 See https://hub.allspice.io/api/swagger#/repository/repoListStatuses 1450 """ 1451 1452 if isinstance(commit, Commit): 1453 commit = commit.sha 1454 1455 params = {} 1456 if sort is not None: 1457 params["sort"] = sort.value 1458 if state is not None: 1459 params["state"] = state.value 1460 1461 url = self.REPO_GET_COMMIT_STATUS.format( 1462 owner=self.owner.username, repo=self.name, sha=commit 1463 ) 1464 response = self.allspice_client.requests_get_paginated(url, params=params) 1465 return [CommitStatus.parse_response(self.allspice_client, status) for status in response] 1466 1467 def create_commit_status( 1468 self, 1469 commit: Union[str, Commit], 1470 context: Optional[str] = None, 1471 description: Optional[str] = None, 1472 state: Optional[CommitStatusState] = None, 1473 target_url: Optional[str] = None, 1474 ) -> CommitStatus: 1475 """ 1476 Create a status on a commit. 1477 1478 See https://hub.allspice.io/api/swagger#/repository/repoCreateStatus 1479 """ 1480 1481 if isinstance(commit, Commit): 1482 commit = commit.sha 1483 1484 data = {} 1485 if context is not None: 1486 data["context"] = context 1487 if description is not None: 1488 data["description"] = description 1489 if state is not None: 1490 data["state"] = state.value 1491 if target_url is not None: 1492 data["target_url"] = target_url 1493 1494 url = self.REPO_GET_COMMIT_STATUS.format( 1495 owner=self.owner.username, repo=self.name, sha=commit 1496 ) 1497 response = self.allspice_client.requests_post(url, data=data) 1498 return CommitStatus.parse_response(self.allspice_client, response) 1499 1500 def delete(self): 1501 self.allspice_client.requests_delete( 1502 Repository.REPO_DELETE % (self.owner.username, self.name) 1503 ) 1504 self.deleted = True
623 @classmethod 624 def search( 625 cls, 626 allspice_client, 627 query: Optional[str] = None, 628 topic: bool = False, 629 include_description: bool = False, 630 user: Optional[User] = None, 631 owner_to_prioritize: Union[User, Organization, None] = None, 632 ) -> list[Repository]: 633 """ 634 Search for repositories. 635 636 See https://hub.allspice.io/api/swagger#/repository/repoSearch 637 638 :param query: The query string to search for 639 :param topic: If true, the query string will only be matched against the 640 repository's topic. 641 :param include_description: If true, the query string will be matched 642 against the repository's description as well. 643 :param user: If specified, only repositories that this user owns or 644 contributes to will be searched. 645 :param owner_to_prioritize: If specified, repositories owned by the 646 given entity will be prioritized in the search. 647 :returns: All repositories matching the query. If there are many 648 repositories matching this query, this may take some time. 649 """ 650 651 params = {} 652 653 if query is not None: 654 params["q"] = query 655 if topic: 656 params["topic"] = topic 657 if include_description: 658 params["include_description"] = include_description 659 if user is not None: 660 params["user"] = user.id 661 if owner_to_prioritize is not None: 662 params["owner_to_prioritize"] = owner_to_prioritize.id 663 664 responses = allspice_client.requests_get_paginated(cls.REPO_SEARCH, params=params) 665 666 return [Repository.parse_response(allspice_client, response) for response in responses]
Search for repositories.
See allspice.allspice.io/api/swagger#/repository/repoSearch">https://huballspice.allspice.io/api/swagger#/repository/repoSearch
Parameters
- query: The query string to search for
- topic: If true, the query string will only be matched against the repository's topic.
- include_description: If true, the query string will be matched against the repository's description as well.
- user: If specified, only repositories that this user owns or contributes to will be searched.
- owner_to_prioritize: If specified, repositories owned by the given entity will be prioritized in the search. :returns: All repositories matching the query. If there are many repositories matching this query, this may take some time.
702 def get_branches(self) -> List["Branch"]: 703 """Get all the Branches of this Repository.""" 704 705 results = self.allspice_client.requests_get_paginated( 706 Repository.REPO_BRANCHES % (self.owner.username, self.name) 707 ) 708 return [Branch.parse_response(self.allspice_client, result) for result in results]
Get all the Branches of this Repository.
710 def get_branch(self, name: str) -> "Branch": 711 """Get a specific Branch of this Repository.""" 712 result = self.allspice_client.requests_get( 713 Repository.REPO_BRANCH.format(owner=self.owner.username, repo=self.name, branch=name) 714 ) 715 return Branch.parse_response(self.allspice_client, result)
Get a specific Branch of this Repository.
717 def add_branch(self, create_from: Ref, newname: str) -> "Branch": 718 """Add a branch to the repository""" 719 # Note: will only work with gitea 1.13 or higher! 720 721 ref_name = Util.data_params_for_ref(create_from) 722 if "ref" not in ref_name: 723 raise ValueError("create_from must be a Branch, Commit or string") 724 ref_name = ref_name["ref"] 725 726 data = {"new_branch_name": newname, "old_ref_name": ref_name} 727 result = self.allspice_client.requests_post( 728 Repository.REPO_BRANCHES % (self.owner.username, self.name), data=data 729 ) 730 return Branch.parse_response(self.allspice_client, result)
Add a branch to the repository
732 def get_issues( 733 self, 734 state: Literal["open", "closed", "all"] = "all", 735 search_query: Optional[str] = None, 736 labels: Optional[List[str]] = None, 737 milestones: Optional[List[Union[Milestone, str]]] = None, 738 assignee: Optional[Union[User, str]] = None, 739 since: Optional[datetime] = None, 740 before: Optional[datetime] = None, 741 ) -> List["Issue"]: 742 """ 743 Get all Issues of this Repository (open and closed) 744 745 https://hub.allspice.io/api/swagger#/repository/repoListIssues 746 747 All params of this method are optional filters. If you don't specify a filter, it 748 will not be applied. 749 750 :param state: The state of the Issues to get. If None, all Issues are returned. 751 :param search_query: Filter issues by text. This is equivalent to searching for 752 `search_query` in the Issues on the web interface. 753 :param labels: Filter issues by labels. 754 :param milestones: Filter issues by milestones. 755 :param assignee: Filter issues by the assigned user. 756 :param since: Filter issues by the date they were created. 757 :param before: Filter issues by the date they were created. 758 :return: A list of Issues. 759 """ 760 761 data = { 762 "state": state, 763 } 764 if search_query: 765 data["q"] = search_query 766 if labels: 767 data["labels"] = ",".join(labels) 768 if milestones: 769 data["milestone"] = ",".join( 770 [ 771 milestone.name if isinstance(milestone, Milestone) else milestone 772 for milestone in milestones 773 ] 774 ) 775 if assignee: 776 if isinstance(assignee, User): 777 data["assignee"] = assignee.username 778 else: 779 data["assignee"] = assignee 780 if since: 781 data["since"] = Util.format_time(since) 782 if before: 783 data["before"] = Util.format_time(before) 784 785 results = self.allspice_client.requests_get_paginated( 786 Repository.REPO_ISSUES.format(owner=self.owner.username, repo=self.name), 787 params=data, 788 ) 789 790 issues = [] 791 for result in results: 792 issue = Issue.parse_response(self.allspice_client, result) 793 # See Issue.request 794 setattr(issue, "_repository", self) 795 # This is mostly for compatibility with an older implementation 796 Issue._add_read_property("repo", self, issue) 797 issues.append(issue) 798 799 return issues
Get all Issues of this Repository (open and closed)
allspice.allspice.io/api/swagger#/repository/repoListIssues">https://huballspice.allspice.io/api/swagger#/repository/repoListIssues
All params of this method are optional filters. If you don't specify a filter, it will not be applied.
Parameters
- state: The state of the Issues to get. If None, all Issues are returned.
- search_query: Filter issues by text. This is equivalent to searching for
search_queryin the Issues on the web interface. - labels: Filter issues by labels.
- milestones: Filter issues by milestones.
- assignee: Filter issues by the assigned user.
- since: Filter issues by the date they were created.
- before: Filter issues by the date they were created.
Returns
A list of Issues.
801 def get_design_reviews( 802 self, 803 state: Literal["open", "closed", "all"] = "all", 804 milestone: Optional[Union[Milestone, str]] = None, 805 labels: Optional[List[str]] = None, 806 ) -> List["DesignReview"]: 807 """ 808 Get all Design Reviews of this Repository. 809 810 https://hub.allspice.io/api/swagger#/repository/repoListPullRequests 811 812 :param state: The state of the Design Reviews to get. If None, all Design Reviews 813 are returned. 814 :param milestone: The milestone of the Design Reviews to get. 815 :param labels: A list of label IDs to filter DRs by. 816 :return: A list of Design Reviews. 817 """ 818 819 params = { 820 "state": state, 821 } 822 if milestone: 823 if isinstance(milestone, Milestone): 824 params["milestone"] = milestone.name 825 else: 826 params["milestone"] = milestone 827 if labels: 828 params["labels"] = ",".join(labels) 829 830 results = self.allspice_client.requests_get_paginated( 831 self.REPO_DESIGN_REVIEWS.format(owner=self.owner.username, repo=self.name), 832 params=params, 833 ) 834 return [DesignReview.parse_response(self.allspice_client, result) for result in results]
Get all Design Reviews of this Repository.
allspice.allspice.io/api/swagger#/repository/repoListPullRequests">https://huballspice.allspice.io/api/swagger#/repository/repoListPullRequests
Parameters
- state: The state of the Design Reviews to get. If None, all Design Reviews are returned.
- milestone: The milestone of the Design Reviews to get.
- labels: A list of label IDs to filter DRs by.
Returns
A list of Design Reviews.
836 def get_commits( 837 self, 838 sha: Optional[str] = None, 839 path: Optional[str] = None, 840 stat: bool = True, 841 ) -> List["Commit"]: 842 """ 843 Get all the Commits of this Repository. 844 845 https://hub.allspice.io/api/swagger#/repository/repoGetAllCommits 846 847 :param sha: The SHA of the commit to start listing commits from. 848 :param path: filepath of a file/dir. 849 :param stat: Include the number of additions and deletions in the response. 850 Disable for speedup. 851 :return: A list of Commits. 852 """ 853 854 data = {} 855 if sha: 856 data["sha"] = sha 857 if path: 858 data["path"] = path 859 if not stat: 860 data["stat"] = False 861 862 try: 863 results = self.allspice_client.requests_get_paginated( 864 Repository.REPO_COMMITS % (self.owner.username, self.name), 865 params=data, 866 ) 867 except ConflictException as err: 868 logging.warning(err) 869 logging.warning("Repository %s/%s is Empty" % (self.owner.username, self.name)) 870 results = [] 871 return [Commit.parse_response(self.allspice_client, result) for result in results]
Get all the Commits of this Repository.
allspice.allspice.io/api/swagger#/repository/repoGetAllCommits">https://huballspice.allspice.io/api/swagger#/repository/repoGetAllCommits
Parameters
- sha: The SHA of the commit to start listing commits from.
- path: filepath of a file/dir.
- stat: Include the number of additions and deletions in the response. Disable for speedup.
Returns
A list of Commits.
873 def get_issues_state(self, state) -> List["Issue"]: 874 """ 875 DEPRECATED: Use get_issues() instead. 876 877 Get issues of state Issue.open or Issue.closed of a repository. 878 """ 879 880 assert state in [Issue.OPENED, Issue.CLOSED] 881 issues = [] 882 data = {"state": state} 883 results = self.allspice_client.requests_get_paginated( 884 Repository.REPO_ISSUES.format(owner=self.owner.username, repo=self.name), 885 params=data, 886 ) 887 for result in results: 888 issue = Issue.parse_response(self.allspice_client, result) 889 # adding data not contained in the issue response 890 # See Issue.request() 891 setattr(issue, "_repository", self) 892 Issue._add_read_property("repo", self, issue) 893 Issue._add_read_property("owner", self.owner, issue) 894 issues.append(issue) 895 return issues
DEPRECATED: Use get_issues() instead.
Get issues of state Issue.open or Issue.closed of a repository.
903 def get_user_time(self, username) -> float: 904 if isinstance(username, User): 905 username = username.username 906 results = self.allspice_client.requests_get( 907 Repository.REPO_USER_TIME % (self.owner.username, self.name, username) 908 ) 909 time = sum(r["time"] for r in results) 910 return time
915 def create_issue(self, title, assignees=frozenset(), description="") -> ApiObject: 916 data = { 917 "assignees": assignees, 918 "body": description, 919 "closed": False, 920 "title": title, 921 } 922 result = self.allspice_client.requests_post( 923 Repository.REPO_ISSUES.format(owner=self.owner.username, repo=self.name), 924 data=data, 925 ) 926 927 issue = Issue.parse_response(self.allspice_client, result) 928 setattr(issue, "_repository", self) 929 Issue._add_read_property("repo", self, issue) 930 return issue
932 def create_design_review( 933 self, 934 title: str, 935 head: Union[Branch, str], 936 base: Union[Branch, str], 937 assignees: Optional[Set[Union[User, str]]] = None, 938 body: Optional[str] = None, 939 due_date: Optional[datetime] = None, 940 milestone: Optional["Milestone"] = None, 941 ) -> "DesignReview": 942 """ 943 Create a new Design Review. 944 945 See https://hub.allspice.io/api/swagger#/repository/repoCreatePullRequest 946 947 :param title: Title of the Design Review 948 :param head: Branch or name of the branch to merge into the base branch 949 :param base: Branch or name of the branch to merge into 950 :param assignees: Optional. A list of users to assign this review. List can be of 951 User objects or of usernames. 952 :param body: An Optional Description for the Design Review. 953 :param due_date: An Optional Due date for the Design Review. 954 :param milestone: An Optional Milestone for the Design Review 955 :return: The created Design Review 956 """ 957 958 data: dict[str, Any] = { 959 "title": title, 960 } 961 962 if isinstance(head, Branch): 963 data["head"] = head.name 964 else: 965 data["head"] = head 966 if isinstance(base, Branch): 967 data["base"] = base.name 968 else: 969 data["base"] = base 970 if assignees: 971 data["assignees"] = [a.username if isinstance(a, User) else a for a in assignees] 972 if body: 973 data["body"] = body 974 if due_date: 975 data["due_date"] = Util.format_time(due_date) 976 if milestone: 977 data["milestone"] = milestone.id 978 979 result = self.allspice_client.requests_post( 980 self.REPO_DESIGN_REVIEWS.format(owner=self.owner.username, repo=self.name), 981 data=data, 982 ) 983 984 return DesignReview.parse_response(self.allspice_client, result)
Create a new Design Review.
See allspice.allspice.io/api/swagger#/repository/repoCreatePullRequest">https://huballspice.allspice.io/api/swagger#/repository/repoCreatePullRequest
Parameters
- title: Title of the Design Review
- head: Branch or name of the branch to merge into the base branch
- base: Branch or name of the branch to merge into
- assignees: Optional. A list of users to assign this review. List can be of User objects or of usernames.
- body: An Optional Description for the Design Review.
- due_date: An Optional Due date for the Design Review.
- milestone: An Optional Milestone for the Design Review
Returns
The created Design Review
986 def create_milestone( 987 self, 988 title: str, 989 description: str, 990 due_date: Optional[str] = None, 991 state: str = "open", 992 ) -> "Milestone": 993 url = Repository.REPO_MILESTONES.format(owner=self.owner.username, repo=self.name) 994 data = {"title": title, "description": description, "state": state} 995 if due_date: 996 data["due_date"] = due_date 997 result = self.allspice_client.requests_post(url, data=data) 998 return Milestone.parse_response(self.allspice_client, result)
1000 def create_gitea_hook(self, hook_url: str, events: List[str]): 1001 url = f"/repos/{self.owner.username}/{self.name}/hooks" 1002 data = { 1003 "type": "gitea", 1004 "config": {"content_type": "json", "url": hook_url}, 1005 "events": events, 1006 "active": True, 1007 } 1008 return self.allspice_client.requests_post(url, data=data)
1018 def is_collaborator(self, username) -> bool: 1019 if isinstance(username, User): 1020 username = username.username 1021 try: 1022 # returns 204 if its ok, 404 if its not 1023 self.allspice_client.requests_get( 1024 Repository.REPO_IS_COLLABORATOR % (self.owner.username, self.name, username) 1025 ) 1026 return True 1027 except Exception: 1028 return False
1030 def get_users_with_access(self) -> Sequence[User]: 1031 url = f"/repos/{self.owner.username}/{self.name}/collaborators" 1032 response = self.allspice_client.requests_get(url) 1033 collabs = [User.parse_response(self.allspice_client, user) for user in response] 1034 if isinstance(self.owner, User): 1035 return [*collabs, self.owner] 1036 else: 1037 # owner must be org 1038 teams = self.owner.get_teams() 1039 for team in teams: 1040 team_repos = team.get_repos() 1041 if self.name in [n.name for n in team_repos]: 1042 collabs += team.get_members() 1043 return collabs
1049 def transfer_ownership( 1050 self, 1051 new_owner: Union[User, Organization], 1052 new_teams: Set[Team] | FrozenSet[Team] = frozenset(), 1053 ): 1054 url = Repository.REPO_TRANSFER.format(owner=self.owner.username, repo=self.name) 1055 data: dict[str, Any] = {"new_owner": new_owner.username} 1056 if isinstance(new_owner, Organization): 1057 new_team_ids = [team.id for team in new_teams if team in new_owner.get_teams()] 1058 data["team_ids"] = new_team_ids 1059 self.allspice_client.requests_post(url, data=data) 1060 # TODO: make sure this instance is either updated or discarded
1062 def get_git_content( 1063 self, 1064 ref: Optional["Ref"] = None, 1065 commit: "Optional[Commit]" = None, 1066 ) -> List[Content]: 1067 """ 1068 Get the metadata for all files in the root directory. 1069 1070 https://hub.allspice.io/api/swagger#/repository/repoGetContentsList 1071 1072 :param ref: branch or commit to get content from 1073 :param commit: commit to get content from (deprecated) 1074 """ 1075 url = f"/repos/{self.owner.username}/{self.name}/contents" 1076 data = Util.data_params_for_ref(ref or commit) 1077 1078 result = [ 1079 Content.parse_response(self.allspice_client, f) 1080 for f in self.allspice_client.requests_get(url, data) 1081 ] 1082 return result
Get the metadata for all files in the root directory.
allspice.allspice.io/api/swagger#/repository/repoGetContentsList">https://huballspice.allspice.io/api/swagger#/repository/repoGetContentsList
Parameters
- ref: branch or commit to get content from
- commit: commit to get content from (deprecated)
1084 def get_tree(self, ref: Optional[Ref] = None, recursive: bool = False) -> List[GitEntry]: 1085 """ 1086 Get the repository's tree on a given ref. 1087 1088 By default, this will only return the top-level entries in the tree. If you want 1089 to get the entire tree, set `recursive` to True. 1090 1091 :param ref: The ref to get the tree from. If not provided, the default branch is used. 1092 :param recursive: Whether to get the entire tree or just the top-level entries. 1093 """ 1094 1095 ref = Util.data_params_for_ref(ref).get("ref", self.default_branch) 1096 url = self.REPO_GET_TREE.format(owner=self.owner.username, repo=self.name, ref=ref) 1097 params = {"recursive": recursive} 1098 results = self.allspice_client.requests_get_paginated(url, params=params) 1099 return [GitEntry.parse_response(self.allspice_client, result) for result in results]
Get the repository's tree on a given ref.
By default, this will only return the top-level entries in the tree. If you want
to get the entire tree, set recursive to True.
Parameters
- ref: The ref to get the tree from. If not provided, the default branch is used.
- recursive: Whether to get the entire tree or just the top-level entries.
1101 def get_file_content( 1102 self, 1103 content: Content, 1104 ref: Optional[Ref] = None, 1105 commit: Optional[Commit] = None, 1106 ) -> Union[str, List["Content"]]: 1107 """https://hub.allspice.io/api/swagger#/repository/repoGetContents""" 1108 url = f"/repos/{self.owner.username}/{self.name}/contents/{content.path}" 1109 data = Util.data_params_for_ref(ref or commit) 1110 1111 if content.type == Content.FILE: 1112 return self.allspice_client.requests_get(url, data)["content"] 1113 else: 1114 return [ 1115 Content.parse_response(self.allspice_client, f) 1116 for f in self.allspice_client.requests_get(url, data) 1117 ]
allspice.allspice.io/api/swagger#/repository/repoGetContents">https://huballspice.allspice.io/api/swagger#/repository/repoGetContents
1119 def get_raw_file( 1120 self, 1121 file_path: str, 1122 ref: Optional[Ref] = None, 1123 ) -> bytes: 1124 """ 1125 Get the raw, binary data of a single file. 1126 1127 Note 1: if the file you are requesting is a text file, you might want to 1128 use .decode() on the result to get a string. For example: 1129 1130 content = repo.get_raw_file("file.txt").decode("utf-8") 1131 1132 Note 2: this method will store the entire file in memory. If you want 1133 to download a large file, you might want to use `download_to_file` 1134 instead. 1135 1136 See https://hub.allspice.io/api/swagger#/repository/repoGetRawFileOrLFS 1137 1138 :param file_path: The path to the file to get. 1139 :param ref: The branch or commit to get the file from. If not provided, 1140 the default branch is used. 1141 """ 1142 1143 url = self.REPO_GET_MEDIA.format( 1144 owner=self.owner.username, 1145 repo=self.name, 1146 path=file_path, 1147 ) 1148 params = Util.data_params_for_ref(ref) 1149 return self.allspice_client.requests_get_raw(url, params=params)
Get the raw, binary data of a single file.
Note 1: if the file you are requesting is a text file, you might want to use .decode() on the result to get a string. For example:
content = repo.get_raw_file("file.txt").decode("utf-8")
Note 2: this method will store the entire file in memory. If you want
to download a large file, you might want to use download_to_file
instead.
See allspice.allspice.io/api/swagger#/repository/repoGetRawFileOrLFS">https://huballspice.allspice.io/api/swagger#/repository/repoGetRawFileOrLFS
Parameters
- file_path: The path to the file to get.
- ref: The branch or commit to get the file from. If not provided, the default branch is used.
1151 def download_to_file( 1152 self, 1153 file_path: str, 1154 io: IO, 1155 ref: Optional[Ref] = None, 1156 ) -> None: 1157 """ 1158 Download the binary data of a file to a file-like object. 1159 1160 Example: 1161 1162 with open("schematic.DSN", "wb") as f: 1163 Repository.download_to_file("Schematics/my_schematic.DSN", f) 1164 1165 :param file_path: The path to the file in the repository from the root 1166 of the repository. 1167 :param io: The file-like object to write the data to. 1168 """ 1169 1170 url = self.allspice_client._AllSpice__get_url( 1171 self.REPO_GET_MEDIA.format( 1172 owner=self.owner.username, 1173 repo=self.name, 1174 path=file_path, 1175 ) 1176 ) 1177 params = Util.data_params_for_ref(ref) 1178 response = self.allspice_client.requests.get( 1179 url, 1180 params=params, 1181 headers=self.allspice_client.headers, 1182 stream=True, 1183 ) 1184 1185 for chunk in response.iter_content(chunk_size=4096): 1186 if chunk: 1187 io.write(chunk)
Download the binary data of a file to a file-like object.
Example:
with open("schematic.DSN", "wb") as f:
Repository.download_to_file("Schematics/my_schematic.DSN", f)
Parameters
- file_path: The path to the file in the repository from the root of the repository.
- io: The file-like object to write the data to.
1189 def get_generated_json( 1190 self, 1191 content: Union[Content, str], 1192 ref: Optional[Ref] = None, 1193 params: Optional[dict] = None, 1194 ) -> dict: 1195 """ 1196 Get the json blob for a cad file if it exists, otherwise enqueue 1197 a new job and return a 503 status. 1198 1199 WARNING: This is still experimental and not recommended for critical 1200 applications. The structure and content of the returned dictionary can 1201 change at any time. 1202 1203 See https://hub.allspice.io/api/swagger#/repository/repoGetAllSpiceJSON 1204 """ 1205 1206 if isinstance(content, Content): 1207 content = content.path 1208 1209 url = self.REPO_GET_ALLSPICE_JSON.format( 1210 owner=self.owner.username, 1211 repo=self.name, 1212 content=content, 1213 ) 1214 data = Util.data_params_for_ref(ref) 1215 if params: 1216 data.update(params) 1217 if self.allspice_client.use_new_schdoc_renderer is not None: 1218 data["use_new_schdoc_renderer"] = ( 1219 "true" if self.allspice_client.use_new_schdoc_renderer else "false" 1220 ) 1221 return self.allspice_client.requests_get(url, data)
Get the json blob for a cad file if it exists, otherwise enqueue a new job and return a 503 status.
WARNING: This is still experimental and not recommended for critical applications. The structure and content of the returned dictionary can change at any time.
See allspice.allspice.io/api/swagger#/repository/repoGetAllSpiceJSON">https://huballspice.allspice.io/api/swagger#/repository/repoGetAllSpiceJSON
1223 def get_generated_svg( 1224 self, 1225 content: Union[Content, str], 1226 ref: Optional[Ref] = None, 1227 params: Optional[dict] = None, 1228 ) -> bytes: 1229 """ 1230 Get the svg blob for a cad file if it exists, otherwise enqueue 1231 a new job and return a 503 status. 1232 1233 WARNING: This is still experimental and not yet recommended for 1234 critical applications. The content of the returned svg can change 1235 at any time. 1236 1237 See https://hub.allspice.io/api/swagger#/repository/repoGetAllSpiceSVG 1238 """ 1239 1240 if isinstance(content, Content): 1241 content = content.path 1242 1243 url = self.REPO_GET_ALLSPICE_SVG.format( 1244 owner=self.owner.username, 1245 repo=self.name, 1246 content=content, 1247 ) 1248 data = Util.data_params_for_ref(ref) 1249 if params: 1250 data.update(params) 1251 if self.allspice_client.use_new_schdoc_renderer is not None: 1252 data["use_new_schdoc_renderer"] = ( 1253 "true" if self.allspice_client.use_new_schdoc_renderer else "false" 1254 ) 1255 return self.allspice_client.requests_get_raw(url, data)
Get the svg blob for a cad file if it exists, otherwise enqueue a new job and return a 503 status.
WARNING: This is still experimental and not yet recommended for critical applications. The content of the returned svg can change at any time.
See allspice.allspice.io/api/swagger#/repository/repoGetAllSpiceSVG">https://huballspice.allspice.io/api/swagger#/repository/repoGetAllSpiceSVG
1257 def get_generated_projectdata( 1258 self, 1259 content: Union[Content, str], 1260 ref: Optional[Ref] = None, 1261 params: Optional[dict] = None, 1262 ) -> dict: 1263 """ 1264 Get the json project data based on the cad file provided 1265 1266 WARNING: This is still experimental and not yet recommended for 1267 critical applications. The content of the returned dictionary can change 1268 at any time. 1269 1270 See https://hub.allspice.io/api/swagger#/repository/repoGetAllSpiceProject 1271 """ 1272 if isinstance(content, Content): 1273 content = content.path 1274 1275 url = self.REPO_GET_ALLSPICE_PROJECT.format( 1276 owner=self.owner.username, 1277 repo=self.name, 1278 content=content, 1279 ) 1280 data = Util.data_params_for_ref(ref) 1281 if params: 1282 data.update(params) 1283 return self.allspice_client.requests_get(url, data)
Get the json project data based on the cad file provided
WARNING: This is still experimental and not yet recommended for critical applications. The content of the returned dictionary can change at any time.
See allspice.allspice.io/api/swagger#/repository/repoGetAllSpiceProject">https://huballspice.allspice.io/api/swagger#/repository/repoGetAllSpiceProject
1285 def create_file(self, file_path: str, content: str, data: Optional[dict] = None): 1286 """https://hub.allspice.io/api/swagger#/repository/repoCreateFile""" 1287 if not data: 1288 data = {} 1289 url = f"/repos/{self.owner.username}/{self.name}/contents/{file_path}" 1290 data.update({"content": content}) 1291 return self.allspice_client.requests_post(url, data)
allspice.allspice.io/api/swagger#/repository/repoCreateFile">https://huballspice.allspice.io/api/swagger#/repository/repoCreateFile
1293 def change_file(self, file_path: str, file_sha: str, content: str, data: Optional[dict] = None): 1294 """https://hub.allspice.io/api/swagger#/repository/repoCreateFile""" 1295 if not data: 1296 data = {} 1297 url = f"/repos/{self.owner.username}/{self.name}/contents/{file_path}" 1298 data.update({"sha": file_sha, "content": content}) 1299 return self.allspice_client.requests_put(url, data)
allspice.allspice.io/api/swagger#/repository/repoCreateFile">https://huballspice.allspice.io/api/swagger#/repository/repoCreateFile
1301 def delete_file(self, file_path: str, file_sha: str, data: Optional[dict] = None): 1302 """https://hub.allspice.io/api/swagger#/repository/repoDeleteFile""" 1303 if not data: 1304 data = {} 1305 url = f"/repos/{self.owner.username}/{self.name}/contents/{file_path}" 1306 data.update({"sha": file_sha}) 1307 return self.allspice_client.requests_delete(url, data)
allspice.allspice.io/api/swagger#/repository/repoDeleteFile">https://huballspice.allspice.io/api/swagger#/repository/repoDeleteFile
1309 def get_archive( 1310 self, 1311 ref: Ref = "main", 1312 archive_format: ArchiveFormat = ArchiveFormat.ZIP, 1313 ) -> bytes: 1314 """ 1315 Download all the files in a specific ref of a repository as a zip or tarball 1316 archive. 1317 1318 https://hub.allspice.io/api/swagger#/repository/repoGetArchive 1319 1320 :param ref: branch or commit to get content from, defaults to the "main" branch 1321 :param archive_format: zip or tar, defaults to zip 1322 """ 1323 1324 ref_string = Util.data_params_for_ref(ref)["ref"] 1325 url = self.REPO_GET_ARCHIVE.format( 1326 owner=self.owner.username, 1327 repo=self.name, 1328 ref=ref_string, 1329 format=archive_format.value, 1330 ) 1331 return self.allspice_client.requests_get_raw(url)
Download all the files in a specific ref of a repository as a zip or tarball archive.
allspice.allspice.io/api/swagger#/repository/repoGetArchive">https://huballspice.allspice.io/api/swagger#/repository/repoGetArchive
Parameters
- ref: branch or commit to get content from, defaults to the "main" branch
- archive_format: zip or tar, defaults to zip
1333 def get_topics(self) -> list[str]: 1334 """ 1335 Gets the list of topics on this repository. 1336 1337 See http://localhost:3000/api/swagger#/repository/repoListTopics 1338 """ 1339 1340 url = self.REPO_GET_TOPICS.format( 1341 owner=self.owner.username, 1342 repo=self.name, 1343 ) 1344 return self.allspice_client.requests_get(url)["topics"]
Gets the list of topics on this repository.
See http://localhost:3000/api/swagger#/repository/repoListTopics
1346 def add_topic(self, topic: str): 1347 """ 1348 Adds a topic to the repository. 1349 1350 See https://hub.allspice.io/api/swagger#/repository/repoAddTopic 1351 1352 :param topic: The topic to add. Topic names must consist only of 1353 lowercase letters, numnbers and dashes (-), and cannot start with 1354 dashes. Topic names also must be under 35 characters long. 1355 """ 1356 1357 url = self.REPO_ADD_TOPIC.format(owner=self.owner.username, repo=self.name, topic=topic) 1358 self.allspice_client.requests_put(url)
Adds a topic to the repository.
See allspice.allspice.io/api/swagger#/repository/repoAddTopic">https://huballspice.allspice.io/api/swagger#/repository/repoAddTopic
Parameters
- topic: The topic to add. Topic names must consist only of lowercase letters, numnbers and dashes (-), and cannot start with dashes. Topic names also must be under 35 characters long.
1360 def create_release( 1361 self, 1362 tag_name: str, 1363 name: Optional[str] = None, 1364 body: Optional[str] = None, 1365 draft: bool = False, 1366 ): 1367 """ 1368 Create a release for this repository. The release will be created for 1369 the tag with the given name. If there is no tag with this name, create 1370 the tag first. 1371 1372 See https://hub.allspice.io/api/swagger#/repository/repoCreateRelease 1373 """ 1374 1375 url = self.REPO_GET_RELEASES.format(owner=self.owner.username, repo=self.name) 1376 data = { 1377 "tag_name": tag_name, 1378 "draft": draft, 1379 } 1380 if name is not None: 1381 data["name"] = name 1382 if body is not None: 1383 data["body"] = body 1384 response = self.allspice_client.requests_post(url, data) 1385 return Release.parse_response(self.allspice_client, response, self)
Create a release for this repository. The release will be created for the tag with the given name. If there is no tag with this name, create the tag first.
See allspice.allspice.io/api/swagger#/repository/repoCreateRelease">https://huballspice.allspice.io/api/swagger#/repository/repoCreateRelease
1387 def get_releases( 1388 self, draft: Optional[bool] = None, pre_release: Optional[bool] = None 1389 ) -> List[Release]: 1390 """ 1391 Get the list of releases for this repository. 1392 1393 See https://hub.allspice.io/api/swagger#/repository/repoListReleases 1394 """ 1395 1396 data = {} 1397 1398 if draft is not None: 1399 data["draft"] = draft 1400 if pre_release is not None: 1401 data["pre-release"] = pre_release 1402 1403 url = self.REPO_GET_RELEASES.format(owner=self.owner.username, repo=self.name) 1404 responses = self.allspice_client.requests_get_paginated(url, params=data) 1405 1406 return [ 1407 Release.parse_response(self.allspice_client, response, self) for response in responses 1408 ]
Get the list of releases for this repository.
See allspice.allspice.io/api/swagger#/repository/repoListReleases">https://huballspice.allspice.io/api/swagger#/repository/repoListReleases
1410 def get_latest_release(self) -> Release: 1411 """ 1412 Get the latest release for this repository. 1413 1414 See https://hub.allspice.io/api/swagger#/repository/repoGetLatestRelease 1415 """ 1416 1417 url = self.REPO_GET_LATEST_RELEASE.format(owner=self.owner.username, repo=self.name) 1418 response = self.allspice_client.requests_get(url) 1419 release = Release.parse_response(self.allspice_client, response, self) 1420 return release
Get the latest release for this repository.
See allspice.allspice.io/api/swagger#/repository/repoGetLatestRelease">https://huballspice.allspice.io/api/swagger#/repository/repoGetLatestRelease
1422 def get_release_by_tag(self, tag: str) -> Release: 1423 """ 1424 Get a release by its tag. 1425 1426 See https://hub.allspice.io/api/swagger#/repository/repoGetReleaseByTag 1427 """ 1428 1429 url = self.REPO_GET_RELEASE_BY_TAG.format( 1430 owner=self.owner.username, repo=self.name, tag=tag 1431 ) 1432 response = self.allspice_client.requests_get(url) 1433 release = Release.parse_response(self.allspice_client, response, self) 1434 return release
Get a release by its tag.
See allspice.allspice.io/api/swagger#/repository/repoGetReleaseByTag">https://huballspice.allspice.io/api/swagger#/repository/repoGetReleaseByTag
1436 def get_commit_statuses( 1437 self, 1438 commit: Union[str, Commit], 1439 sort: Optional[CommitStatusSort] = None, 1440 state: Optional[CommitStatusState] = None, 1441 ) -> List[CommitStatus]: 1442 """ 1443 Get a list of statuses for a commit. 1444 1445 This is roughly equivalent to the Commit.get_statuses method, but this 1446 method allows you to sort and filter commits and is more convenient if 1447 you have a commit SHA and don't need to get the commit itself. 1448 1449 See https://hub.allspice.io/api/swagger#/repository/repoListStatuses 1450 """ 1451 1452 if isinstance(commit, Commit): 1453 commit = commit.sha 1454 1455 params = {} 1456 if sort is not None: 1457 params["sort"] = sort.value 1458 if state is not None: 1459 params["state"] = state.value 1460 1461 url = self.REPO_GET_COMMIT_STATUS.format( 1462 owner=self.owner.username, repo=self.name, sha=commit 1463 ) 1464 response = self.allspice_client.requests_get_paginated(url, params=params) 1465 return [CommitStatus.parse_response(self.allspice_client, status) for status in response]
Get a list of statuses for a commit.
This is roughly equivalent to the Commit.get_statuses method, but this method allows you to sort and filter commits and is more convenient if you have a commit SHA and don't need to get the commit itself.
See allspice.allspice.io/api/swagger#/repository/repoListStatuses">https://huballspice.allspice.io/api/swagger#/repository/repoListStatuses
1467 def create_commit_status( 1468 self, 1469 commit: Union[str, Commit], 1470 context: Optional[str] = None, 1471 description: Optional[str] = None, 1472 state: Optional[CommitStatusState] = None, 1473 target_url: Optional[str] = None, 1474 ) -> CommitStatus: 1475 """ 1476 Create a status on a commit. 1477 1478 See https://hub.allspice.io/api/swagger#/repository/repoCreateStatus 1479 """ 1480 1481 if isinstance(commit, Commit): 1482 commit = commit.sha 1483 1484 data = {} 1485 if context is not None: 1486 data["context"] = context 1487 if description is not None: 1488 data["description"] = description 1489 if state is not None: 1490 data["state"] = state.value 1491 if target_url is not None: 1492 data["target_url"] = target_url 1493 1494 url = self.REPO_GET_COMMIT_STATUS.format( 1495 owner=self.owner.username, repo=self.name, sha=commit 1496 ) 1497 response = self.allspice_client.requests_post(url, data=data) 1498 return CommitStatus.parse_response(self.allspice_client, response)
Create a status on a commit.
See allspice.allspice.io/api/swagger#/repository/repoCreateStatus">https://huballspice.allspice.io/api/swagger#/repository/repoCreateStatus
574 class ArchiveFormat(Enum): 575 """ 576 Archive formats for Repository.get_archive 577 """ 578 579 TAR = "tar.gz" 580 ZIP = "zip"
Archive formats for Repository.get_archive
582 class CommitStatusSort(Enum): 583 """ 584 Sort order for Repository.get_commit_status 585 """ 586 587 OLDEST = "oldest" 588 RECENT_UPDATE = "recentupdate" 589 LEAST_UPDATE = "leastupdate" 590 LEAST_INDEX = "leastindex" 591 HIGHEST_INDEX = "highestindex"
Sort order for Repository.get_commit_status
2620class Team(ApiObject): 2621 can_create_org_repo: bool 2622 description: str 2623 id: int 2624 includes_all_repositories: bool 2625 name: str 2626 organization: Optional["Organization"] 2627 permission: str 2628 units: List[str] 2629 units_map: Dict[str, str] 2630 2631 API_OBJECT = """/teams/{id}""" # <id> 2632 ADD_REPO = """/teams/%s/repos/%s/%s""" # <id, org, repo> 2633 TEAM_DELETE = """/teams/%s""" # <id> 2634 GET_MEMBERS = """/teams/%s/members""" # <id> 2635 GET_REPOS = """/teams/%s/repos""" # <id> 2636 2637 def __init__(self, allspice_client): 2638 super().__init__(allspice_client) 2639 2640 def __eq__(self, other): 2641 if not isinstance(other, Team): 2642 return False 2643 return self.organization == other.organization and self.id == other.id 2644 2645 def __hash__(self): 2646 return hash(self.organization) ^ hash(self.id) 2647 2648 _fields_to_parsers: ClassVar[dict] = { 2649 "organization": lambda allspice_client, o: Organization.parse_response(allspice_client, o) 2650 } 2651 2652 _patchable_fields: ClassVar[set[str]] = { 2653 "can_create_org_repo", 2654 "description", 2655 "includes_all_repositories", 2656 "name", 2657 "permission", 2658 "units", 2659 "units_map", 2660 } 2661 2662 @classmethod 2663 def request(cls, allspice_client, id: int): 2664 return cls._request(allspice_client, {"id": id}) 2665 2666 def commit(self): 2667 args = {"id": self.id} 2668 self._commit(args) 2669 2670 def add_user(self, user: User): 2671 """https://hub.allspice.io/api/swagger#/organization/orgAddTeamMember""" 2672 url = f"/teams/{self.id}/members/{user.login}" 2673 self.allspice_client.requests_put(url) 2674 2675 def add_repo(self, org: Organization, repo: Union[Repository, str]): 2676 if isinstance(repo, Repository): 2677 repo_name = repo.name 2678 else: 2679 repo_name = repo 2680 self.allspice_client.requests_put(Team.ADD_REPO % (self.id, org.username, repo_name)) 2681 2682 def get_members(self): 2683 """Get all users assigned to the team.""" 2684 results = self.allspice_client.requests_get_paginated( 2685 Team.GET_MEMBERS % self.id, 2686 ) 2687 return [User.parse_response(self.allspice_client, result) for result in results] 2688 2689 def get_repos(self): 2690 """Get all repos of this Team.""" 2691 results = self.allspice_client.requests_get(Team.GET_REPOS % self.id) 2692 return [Repository.parse_response(self.allspice_client, result) for result in results] 2693 2694 def delete(self): 2695 self.allspice_client.requests_delete(Team.TEAM_DELETE % self.id) 2696 self.deleted = True 2697 2698 def remove_team_member(self, user_name: str): 2699 url = f"/teams/{self.id}/members/{user_name}" 2700 self.allspice_client.requests_delete(url)
2670 def add_user(self, user: User): 2671 """https://hub.allspice.io/api/swagger#/organization/orgAddTeamMember""" 2672 url = f"/teams/{self.id}/members/{user.login}" 2673 self.allspice_client.requests_put(url)
allspice.allspice.io/api/swagger#/organization/orgAddTeamMember">https://huballspice.allspice.io/api/swagger#/organization/orgAddTeamMember
2682 def get_members(self): 2683 """Get all users assigned to the team.""" 2684 results = self.allspice_client.requests_get_paginated( 2685 Team.GET_MEMBERS % self.id, 2686 ) 2687 return [User.parse_response(self.allspice_client, result) for result in results]
Get all users assigned to the team.
2689 def get_repos(self): 2690 """Get all repos of this Team.""" 2691 results = self.allspice_client.requests_get(Team.GET_REPOS % self.id) 2692 return [Repository.parse_response(self.allspice_client, result) for result in results]
Get all repos of this Team.
245class User(ApiObject): 246 active: bool 247 admin: Any 248 allow_create_organization: Any 249 allow_git_hook: Any 250 allow_import_local: Any 251 avatar_url: str 252 created: str 253 description: str 254 email: str 255 emails: List[Any] 256 followers_count: int 257 following_count: int 258 full_name: str 259 html_url: str 260 id: int 261 is_admin: bool 262 language: str 263 last_login: str 264 location: str 265 login: str 266 login_name: str 267 max_repo_creation: Any 268 must_change_password: Any 269 password: Any 270 prohibit_login: bool 271 restricted: bool 272 source_id: int 273 starred_repos_count: int 274 username: str 275 visibility: str 276 website: str 277 278 API_OBJECT = """/users/{name}""" # <org> 279 USER_MAIL = """/user/emails?sudo=%s""" # <name> 280 USER_PATCH = """/admin/users/%s""" # <username> 281 ADMIN_DELETE_USER = """/admin/users/%s""" # <username> 282 ADMIN_EDIT_USER = """/admin/users/{username}""" # <username> 283 USER_HEATMAP = """/users/%s/heatmap""" # <username> 284 285 def __init__(self, allspice_client): 286 super().__init__(allspice_client) 287 self._emails = [] 288 289 def __eq__(self, other): 290 if not isinstance(other, User): 291 return False 292 return self.allspice_client == other.allspice_client and self.id == other.id 293 294 def __hash__(self): 295 return hash(self.allspice_client) ^ hash(self.id) 296 297 @property 298 def emails(self): 299 self.__request_emails() 300 return self._emails 301 302 @classmethod 303 def request(cls, allspice_client, name: str) -> "User": 304 api_object = cls._request(allspice_client, {"name": name}) 305 return api_object 306 307 _patchable_fields: ClassVar[set[str]] = { 308 "active", 309 "admin", 310 "allow_create_organization", 311 "allow_git_hook", 312 "allow_import_local", 313 "email", 314 "full_name", 315 "location", 316 "login_name", 317 "max_repo_creation", 318 "must_change_password", 319 "password", 320 "prohibit_login", 321 "website", 322 } 323 324 def commit(self, login_name: str, source_id: int = 0): 325 """ 326 Unfortunately it is necessary to require the login name 327 as well as the login source (that is not supplied when getting a user) for 328 changing a user. 329 Usually source_id is 0 and the login_name is equal to the username. 330 """ 331 values = self.get_dirty_fields() 332 values.update( 333 # api-doc says that the "source_id" is necessary; works without though 334 {"login_name": login_name, "source_id": source_id} 335 ) 336 args = {"username": self.username} 337 self.allspice_client.requests_patch(User.ADMIN_EDIT_USER.format(**args), data=values) 338 self._dirty_fields = {} 339 340 def create_repo( 341 self, 342 repoName: str, 343 description: str = "", 344 private: bool = False, 345 autoInit=True, 346 gitignores: Optional[str] = None, 347 license: Optional[str] = None, 348 readme: str = "Default", 349 issue_labels: Optional[str] = None, 350 default_branch="master", 351 ): 352 """Create a user Repository 353 354 Throws: 355 AlreadyExistsException: If the Repository exists already. 356 Exception: If something else went wrong. 357 """ 358 result = self.allspice_client.requests_post( 359 "/user/repos", 360 data={ 361 "name": repoName, 362 "description": description, 363 "private": private, 364 "auto_init": autoInit, 365 "gitignores": gitignores, 366 "license": license, 367 "issue_labels": issue_labels, 368 "readme": readme, 369 "default_branch": default_branch, 370 }, 371 ) 372 if "id" in result: 373 self.allspice_client.logger.info("Successfully created Repository %s " % result["name"]) 374 else: 375 self.allspice_client.logger.error(result["message"]) 376 raise Exception("Repository not created... (gitea: %s)" % result["message"]) 377 return Repository.parse_response(self.allspice_client, result) 378 379 def get_repositories(self) -> List["Repository"]: 380 """Get all Repositories owned by this User.""" 381 url = f"/users/{self.username}/repos" 382 results = self.allspice_client.requests_get_paginated(url) 383 return [Repository.parse_response(self.allspice_client, result) for result in results] 384 385 def get_orgs(self) -> List[Organization]: 386 """Get all Organizations this user is a member of.""" 387 url = f"/users/{self.username}/orgs" 388 results = self.allspice_client.requests_get_paginated(url) 389 return [Organization.parse_response(self.allspice_client, result) for result in results] 390 391 def get_teams(self) -> List["Team"]: 392 url = "/user/teams" 393 results = self.allspice_client.requests_get_paginated(url, sudo=self) 394 return [Team.parse_response(self.allspice_client, result) for result in results] 395 396 def get_accessible_repos(self) -> List["Repository"]: 397 """Get all Repositories accessible by the logged in User.""" 398 results = self.allspice_client.requests_get("/user/repos", sudo=self) 399 return [Repository.parse_response(self.allspice_client, result) for result in results] 400 401 def __request_emails(self): 402 result = self.allspice_client.requests_get(User.USER_MAIL % self.login) 403 # report if the adress changed by this 404 for mail in result: 405 self._emails.append(mail["email"]) 406 if mail["primary"]: 407 self._email = mail["email"] 408 409 def delete(self): 410 """Deletes this User. Also deletes all Repositories he owns.""" 411 self.allspice_client.requests_delete(User.ADMIN_DELETE_USER % self.username) 412 self.deleted = True 413 414 def get_heatmap(self) -> List[Tuple[datetime, int]]: 415 results = self.allspice_client.requests_get(User.USER_HEATMAP % self.username) 416 results = [ 417 (datetime.fromtimestamp(result["timestamp"]), result["contributions"]) 418 for result in results 419 ] 420 return results
324 def commit(self, login_name: str, source_id: int = 0): 325 """ 326 Unfortunately it is necessary to require the login name 327 as well as the login source (that is not supplied when getting a user) for 328 changing a user. 329 Usually source_id is 0 and the login_name is equal to the username. 330 """ 331 values = self.get_dirty_fields() 332 values.update( 333 # api-doc says that the "source_id" is necessary; works without though 334 {"login_name": login_name, "source_id": source_id} 335 ) 336 args = {"username": self.username} 337 self.allspice_client.requests_patch(User.ADMIN_EDIT_USER.format(**args), data=values) 338 self._dirty_fields = {}
Unfortunately it is necessary to require the login name as well as the login source (that is not supplied when getting a user) for changing a user. Usually source_id is 0 and the login_name is equal to the username.
340 def create_repo( 341 self, 342 repoName: str, 343 description: str = "", 344 private: bool = False, 345 autoInit=True, 346 gitignores: Optional[str] = None, 347 license: Optional[str] = None, 348 readme: str = "Default", 349 issue_labels: Optional[str] = None, 350 default_branch="master", 351 ): 352 """Create a user Repository 353 354 Throws: 355 AlreadyExistsException: If the Repository exists already. 356 Exception: If something else went wrong. 357 """ 358 result = self.allspice_client.requests_post( 359 "/user/repos", 360 data={ 361 "name": repoName, 362 "description": description, 363 "private": private, 364 "auto_init": autoInit, 365 "gitignores": gitignores, 366 "license": license, 367 "issue_labels": issue_labels, 368 "readme": readme, 369 "default_branch": default_branch, 370 }, 371 ) 372 if "id" in result: 373 self.allspice_client.logger.info("Successfully created Repository %s " % result["name"]) 374 else: 375 self.allspice_client.logger.error(result["message"]) 376 raise Exception("Repository not created... (gitea: %s)" % result["message"]) 377 return Repository.parse_response(self.allspice_client, result)
Create a user Repository
Throws: AlreadyExistsException: If the Repository exists already. Exception: If something else went wrong.
379 def get_repositories(self) -> List["Repository"]: 380 """Get all Repositories owned by this User.""" 381 url = f"/users/{self.username}/repos" 382 results = self.allspice_client.requests_get_paginated(url) 383 return [Repository.parse_response(self.allspice_client, result) for result in results]
Get all Repositories owned by this User.
385 def get_orgs(self) -> List[Organization]: 386 """Get all Organizations this user is a member of.""" 387 url = f"/users/{self.username}/orgs" 388 results = self.allspice_client.requests_get_paginated(url) 389 return [Organization.parse_response(self.allspice_client, result) for result in results]
Get all Organizations this user is a member of.
396 def get_accessible_repos(self) -> List["Repository"]: 397 """Get all Repositories accessible by the logged in User.""" 398 results = self.allspice_client.requests_get("/user/repos", sudo=self) 399 return [Repository.parse_response(self.allspice_client, result) for result in results]
Get all Repositories accessible by the logged in User.
409 def delete(self): 410 """Deletes this User. Also deletes all Repositories he owns.""" 411 self.allspice_client.requests_delete(User.ADMIN_DELETE_USER % self.username) 412 self.deleted = True
Deletes this User. Also deletes all Repositories he owns.