This commit is contained in:
Dale Myers 2024-05-08 09:35:02 +01:00
Родитель 2a121994fd
Коммит ba8411986e
13 изменённых файлов: 668 добавлений и 880 удалений

Просмотреть файл

@ -1,42 +1,39 @@
jobs:
- job: "Test"
pool:
vmImage: "ubuntu-latest"
strategy:
matrix:
Python39:
python.version: "3.9"
Python310:
python.version: "3.10"
Python311:
python.version: "3.11"
Python312:
python.version: "3.12"
maxParallel: 4
- job: 'Test'
pool:
vmImage: 'ubuntu-latest'
strategy:
matrix:
Python38:
python.version: '3.8'
Python39:
python.version: '3.9'
Python310:
python.version: '3.10'
Python311:
python.version: '3.11'
maxParallel: 4
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: "$(python.version)"
architecture: "x64"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '$(python.version)'
architecture: 'x64'
- script: curl -sSL https://install.python-poetry.org | python3
displayName: Install Poetry
- script: curl -sSL https://install.python-poetry.org | python3
displayName: Install Poetry
- script: |
poetry install
displayName: "Install dependencies"
- script: |
poetry install
displayName: 'Install dependencies'
- script: poetry run black --line-length 100 --check appcenter tests
displayName: "Run Black"
- script:
poetry run black --check appcenter tests
displayName: 'Run Black'
- script: |
poetry run pylint --rcfile=pylintrc appcenter tests
displayName: "Lint"
- script: |
poetry run pylint --rcfile=pylintrc appcenter tests
displayName: 'Lint'
- script: |
poetry run mypy --ignore-missing-imports appcenter/ tests/
displayName: 'Type Check'
- script: |
poetry run mypy --ignore-missing-imports appcenter/ tests/
displayName: "Type Check"

4
.vscode/settings.json поставляемый
Просмотреть файл

@ -10,4 +10,8 @@
"--line-length",
"100"
],
"black-formatter.args": [
"--line-length",
"100"
],
}

Просмотреть файл

@ -5,13 +5,7 @@
"""App Center API wrapper."""
import json
import logging
import re
import time
from typing import Any, ClassVar, Dict, List, Optional
import requests
from appcenter.account import AppCenterAccountClient
from appcenter.analytics import AppCenterAnalyticsClient
@ -35,9 +29,7 @@ class AppCenterClient:
tokens: AppCenterTokensClient
versions: AppCenterVersionsClient
def __init__(
self, *, access_token: str, parent_logger: Optional[logging.Logger] = None
) -> None:
def __init__(self, *, access_token: str, parent_logger: logging.Logger | None = None) -> None:
"""Initialize the AppCenterClient with the application id and the token."""
if parent_logger is None:

Просмотреть файл

@ -4,7 +4,6 @@
# Licensed under the MIT license.
import logging
from typing import List, Optional
import urllib.parse
import deserialize
@ -23,7 +22,7 @@ class AppCenterAccountClient(AppCenterDerivedClient):
def __init__(self, token: str, parent_logger: logging.Logger) -> None:
super().__init__("account", token, parent_logger)
def users(self, *, owner_name: str, app_name: str) -> List[User]:
def users(self, *, owner_name: str, app_name: str) -> list[User]:
"""Get the users for an app
:param str owner_name: The name of the app account owner
@ -39,7 +38,7 @@ class AppCenterAccountClient(AppCenterDerivedClient):
response = self.get(request_url)
return deserialize.deserialize(List[User], response.json())
return deserialize.deserialize(list[User], response.json())
def add_collaborator(
self,
@ -47,7 +46,7 @@ class AppCenterAccountClient(AppCenterDerivedClient):
owner_name: str,
app_name: str,
user_email: str,
role: Optional[Role] = None,
role: Role | None = None,
) -> None:
"""Add a user as a collaborator to an app.
@ -57,14 +56,12 @@ class AppCenterAccountClient(AppCenterDerivedClient):
:param str owner_name: The name of the app account owner
:param str app_name: The name of the app
:param str user_email: The email of the user
:param Optional[Role] role: The role the user should have (this is required for new users)
:param Role | None role: The role the user should have (this is required for new users)
:returns: The list of users
"""
self.log.info(
f"Adding user {user_email} as collaborator on: {owner_name}/{app_name}"
)
self.log.info(f"Adding user {user_email} as collaborator on: {owner_name}/{app_name}")
request_url = self.generate_url(owner_name=owner_name, app_name=app_name)
request_url += "/invitations"
@ -76,9 +73,7 @@ class AppCenterAccountClient(AppCenterDerivedClient):
self.post(request_url, data=data)
def delete_collaborator(
self, *, owner_name: str, app_name: str, user_email: str
) -> None:
def delete_collaborator(self, *, owner_name: str, app_name: str, user_email: str) -> None:
"""Remove a user as a collaborator from an app.
:param str owner_name: The name of the app account owner

Просмотреть файл

@ -4,7 +4,6 @@
# Licensed under the MIT license.
import logging
from typing import List
import deserialize
@ -27,13 +26,13 @@ class AppCenterAnalyticsClient(AppCenterDerivedClient):
*,
owner_name: str,
app_name: str,
releases: List[ReleaseWithDistributionGroup],
releases: list[ReleaseWithDistributionGroup],
) -> ReleaseCounts:
"""Get the release counts for an app
:param str owner_name: The name of the app account owner
:param str app_name: The name of the app
:param List[ReleaseWithDistributionGroup] releases: The list of releases to get the counts for
:param list[ReleaseWithDistributionGroup] releases: The list of releases to get the counts for
:returns: The release counts
"""

Просмотреть файл

@ -6,7 +6,7 @@
import datetime
import logging
import os
from typing import Any, Dict, Iterator, Optional
from typing import Any, Iterator
import urllib.parse
from azure.storage.blob import BlockBlobService
@ -39,9 +39,7 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
def __init__(self, token: str, parent_logger: logging.Logger) -> None:
super().__init__("crashes", token, parent_logger)
def group_details(
self, *, owner_name: str, app_name: str, error_group_id: str
) -> ErrorGroup:
def group_details(self, *, owner_name: str, app_name: str, error_group_id: str) -> ErrorGroup:
"""Get the error group details.
:param str owner_name: The name of the app account owner
@ -95,13 +93,11 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
"""
request_url = self.generate_url(owner_name=owner_name, app_name=app_name)
request_url += (
f"/errors/errorGroups/{error_group_id}/errors/{error_id}/download"
)
request_url += f"/errors/errorGroups/{error_group_id}/errors/{error_id}/download"
response = self.get(request_url)
return deserialize.deserialize(Dict[str, Any], response.json())
return deserialize.deserialize(dict[str, Any], response.json())
def set_annotation(
self,
@ -110,7 +106,7 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
app_name: str,
error_group_id: str,
annotation: str,
state: Optional[ErrorGroupState] = None,
state: ErrorGroupState | None = None,
) -> None:
"""Get the error group details.
@ -118,7 +114,7 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
:param str app_name: The name of the app
:param str error_group_id: The ID of the error group to set the annotation on
:param str annotation: The annotation text
:param Optional[ErrorGroupState] state: The state to set the error group to
:param ErrorGroupState | None state: The state to set the error group to
The `state` parameter here does seem somewhat unusual, but it can't be
helped unfortunately. The API requires that we set the state with the
@ -141,18 +137,19 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
self.patch(request_url, data={"state": state.value, "annotation": annotation})
# pylint: disable=too-many-arguments
def get_error_groups(
self,
*,
owner_name: str,
app_name: str,
start_time: datetime.datetime,
end_time: Optional[datetime.datetime] = None,
version: Optional[str] = None,
app_build: Optional[str] = None,
group_state: Optional[ErrorGroupState] = None,
error_type: Optional[str] = None,
order_by: Optional[str] = None,
end_time: datetime.datetime | None = None,
version: str | None = None,
app_build: str | None = None,
group_state: ErrorGroupState | None = None,
error_type: str | None = None,
order_by: str | None = None,
limit: int = 30,
) -> Iterator[ErrorGroupListItem]:
"""Get the error groups for an app.
@ -160,13 +157,13 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
:param str owner_name: The name of the app account owner
:param str app_name: The name of the app
:param datetime.datetime start_time: The time to start getting error groups from
:param Optional[datetime.datetime] end_time: The end time to get error groups from
:param Optional[str] version: The version of the app to restrict the search to (if any)
:param Optional[str] app_build: The build to restrict the search to (if any)
:param Optional[ErrorGroupState] group_state: Set to filter to just this group state (open, closed, ignored)
:param Optional[str] error_type: Set to filter to specific types of error (all, unhandledError, handledError)
:param Optional[str] order_by: The order by parameter to pass in (this will be encoded for you)
:param Optional[str] limit: The max number of results to return per request (should not go past 100)
:param datetime.datetime | None end_time: The end time to get error groups from
:param str | None version: The version of the app to restrict the search to (if any)
:param str | None app_build: The build to restrict the search to (if any)
:param ErrorGroupState | None group_state: Set to filter to just this group state (open, closed, ignored)
:param str | None error_type: Set to filter to specific types of error (all, unhandledError, handledError)
:param str | None order_by: The order by parameter to pass in (this will be encoded for you)
:param str | None limit: The max number of results to return per request (should not go past 100)
:returns: An iterator of ErrorGroupListItem
"""
@ -223,26 +220,28 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
# pylint: disable=too-many-locals
# pylint: enable=too-many-arguments
def errors_in_group(
self,
*,
owner_name: str,
app_name: str,
error_group_id: str,
start_time: Optional[datetime.datetime] = None,
end_time: Optional[datetime.datetime] = None,
model: Optional[str] = None,
operating_system: Optional[str] = None,
start_time: datetime.datetime | None = None,
end_time: datetime.datetime | None = None,
model: str | None = None,
operating_system: str | None = None,
) -> Iterator[HandledError]:
"""Get the errors in a group.
:param str owner_name: The name of the app account owner
:param str app_name: The name of the app
:param str error_group_id: The ID of the group to get the errors from
:param Optional[datetime.datetime] start_time: The time to start getting error groups from
:param Optional[datetime.datetime] end_time: The end time to get error groups from
:param Optional[str] model: The device model to restrict the search to (if any)
:param Optional[str] operating_system: The OS to restrict the search to (if any)
:param datetime.datetime | None start_time: The time to start getting error groups from
:param datetime.datetime | None end_time: The end time to get error groups from
:param str | None model: The device model to restrict the search to (if any)
:param str | None operating_system: The OS to restrict the search to (if any)
:returns: An iterator of HandledError
"""
@ -250,7 +249,7 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
request_url = self.generate_url(owner_name=owner_name, app_name=app_name)
request_url += f"/errors/errorGroups/{error_group_id}/errors?"
parameters: Dict[str, str] = {}
parameters: dict[str, str] = {}
if start_time:
parameters["start"] = start_time.replace(microsecond=0).isoformat()
@ -293,8 +292,8 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
app_name: str,
symbols_name: str,
symbol_type: SymbolType,
build_number: Optional[str] = None,
version: Optional[str] = None,
build_number: str | None = None,
version: str | None = None,
) -> SymbolUploadBeginResponse:
"""Upload debug symbols
@ -302,8 +301,8 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
:param str app_name: The name of the app
:param str symbols_path: The path to the symbols
:param str symbol_type: The type of symbols being uploaded
:param Optional[str] build_number: The build number (required for Android)
:param Optional[str] version: The build version (required for Android)
:param str | None build_number: The build number (required for Android)
:param str | None version: The build version (required for Android)
:raises ValueError: If the build number or version aren't specified and it's an Android upload
@ -353,17 +352,18 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
return deserialize.deserialize(SymbolUploadEndRequest, response.json())
# pylint: disable=too-many-arguments
def upload_symbols(
self,
*,
owner_name: str,
app_name: str,
symbols_path: str,
symbols_name: Optional[str] = None,
symbols_name: str | None = None,
symbol_type: SymbolType,
build_number: Optional[str] = None,
version: Optional[str] = None,
progress_callback: Optional[ProgressCallback] = None,
build_number: str | None = None,
version: str | None = None,
progress_callback: ProgressCallback | None = None,
) -> None:
"""Upload debug symbols
@ -372,9 +372,9 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
:param str symbols_path: The path to the symbols
:param str symbols_name: The name to use for the symbols (defaults to file basename)
:param str symbol_type: The type of symbols being uploaded
:param Optional[str] build_number: The build number (required for Android)
:param Optional[str] version: The build version (required for Android)
:param Optional[ProgressCallback] progress_callback: The upload progress callback
:param str | None build_number: The build number (required for Android)
:param str | None version: The build version (required for Android)
:param ProgressCallback | None progress_callback: The upload progress callback
For the upload progress callback, this is a callable where the first
parameter is the number of bytes uploaded, and the second parameter is
@ -403,9 +403,7 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
url_components = urllib.parse.urlparse(begin_upload_response.upload_url)
path = url_components.path[1:]
container, blob_name = path.split("/")
connection_string = (
f"BlobEndpoint={url_components.scheme}://{url_components.netloc};"
)
connection_string = f"BlobEndpoint={url_components.scheme}://{url_components.netloc};"
connection_string += f"SharedAccessSignature={url_components.query}"
service = BlockBlobService(connection_string=connection_string)
service.create_blob_from_stream(
@ -421,9 +419,9 @@ class AppCenterCrashesClient(AppCenterDerivedClient):
if commit_response.status != SymbolUploadStatus.COMMITTED:
raise Exception("Failed to upload symbols")
def _next_link_polished(
self, next_link: str, owner_name: str, app_name: str
) -> str:
# pylint: enable=too-many-arguments
def _next_link_polished(self, next_link: str, owner_name: str, app_name: str) -> str:
"""Polish nextLink string gotten from AppCenter service
:param str next_link: The nextLink property from a service response when items are queried in batches

Просмотреть файл

@ -4,7 +4,7 @@
# Licensed under the MIT license.
import logging
from typing import Any, BinaryIO, Callable, Dict, Optional, Tuple
from typing import Any, BinaryIO, Callable
import deserialize
import requests
@ -20,7 +20,7 @@ from tenacity import (
from appcenter.constants import API_BASE_URL
ProgressCallback = Callable[[int, Optional[int]], None]
ProgressCallback = Callable[[int, int | None], None]
class AppCenterHTTPError:
@ -148,9 +148,7 @@ class AppCenterDerivedClient:
"""
return f"{API_BASE_URL}/v{version}"
def generate_url(
self, *, version: str = "0.1", owner_name: str, app_name: str
) -> str:
def generate_url(self, *, version: str = "0.1", owner_name: str, app_name: str) -> str:
"""Generate a URL to use for querying the API.
:param str version: The API version to hit
@ -206,9 +204,7 @@ class AppCenterDerivedClient:
:raises AppCenterHTTPException: If the request fails with a non 200 status code
"""
response = self.session.patch(
url, headers={"Content-Type": "application/json"}, json=data
)
response = self.session.patch(url, headers={"Content-Type": "application/json"}, json=data)
if response.status_code < 200 or response.status_code >= 300:
raise create_exception(response)
@ -230,9 +226,7 @@ class AppCenterDerivedClient:
:raises AppCenterHTTPException: If the request fails with a non 200 status code
"""
response = self.session.post(
url, headers={"Content-Type": "application/json"}, json=data
)
response = self.session.post(url, headers={"Content-Type": "application/json"}, json=data)
if response.status_code < 200 or response.status_code >= 300:
raise create_exception(response)
@ -266,9 +260,7 @@ class AppCenterDerivedClient:
wait=wait_fixed(10),
stop=stop_after_attempt(3),
)
def post_files(
self, url: str, *, files: Dict[str, Tuple[str, BinaryIO]]
) -> requests.Response:
def post_files(self, url: str, *, files: dict[str, tuple[str, BinaryIO]]) -> requests.Response:
"""Perform a POST request to a url, sending files
:param url: The URL to run the POST on
@ -314,9 +306,7 @@ class AppCenterDerivedClient:
wait=wait_fixed(10),
stop=stop_after_attempt(3),
)
def azure_blob_upload(
self, url: str, *, file_stream: BinaryIO
) -> requests.Response:
def azure_blob_upload(self, url: str, *, file_stream: BinaryIO) -> requests.Response:
"""Upload a file to an Azure Blob Storage URL
:param url: The URL to upload to

Просмотреть файл

@ -5,12 +5,12 @@
import datetime
import enum
from typing import Any, Dict, List, Optional
from typing import Any
import deserialize
def iso8601parse(date_string: Optional[str]) -> Optional[datetime.datetime]:
def iso8601parse(date_string: str | None) -> datetime.datetime | None:
"""Parse an ISO8601 date string into a datetime.
:param date_string: The date string to parse
@ -40,19 +40,19 @@ class HandledErrorReasonFrame:
JAVA = "Java"
UNKNOWN = "Unknown"
className: Optional[str] # name of the class
method: Optional[str] # name of the method
classMethod: Optional[bool] # is a class method
file: Optional[str] # name of the file
line: Optional[int] # line number
appCode: Optional[bool] # this line isn't from any framework
frameworkName: Optional[str] # Name of the framework
codeFormatted: Optional[str] # Formatted frame string
codeRaw: Optional[str] # Unformatted Frame string
methodParams: Optional[str] # parameters of the frames method
exceptionType: Optional[str] # Exception type.
osExceptionType: Optional[str] # OS exception type. (aka. SIGNAL)
language: Optional[ProgrammingLanguage] # programming language of the frame
className: str | None # name of the class
method: str | None # name of the method
classMethod: bool | None # is a class method
file: str | None # name of the file
line: int | None # line number
appCode: bool | None # this line isn't from any framework
frameworkName: str | None # Name of the framework
codeFormatted: str | None # Formatted frame string
codeRaw: str | None # Unformatted Frame string
methodParams: str | None # parameters of the frames method
exceptionType: str | None # Exception type.
osExceptionType: str | None # OS exception type. (aka. SIGNAL)
language: ProgrammingLanguage | None # programming language of the frame
class ErrorGroupState(enum.Enum):
@ -65,89 +65,89 @@ class ErrorGroupState(enum.Enum):
@deserialize.parser("lastOccurrence", iso8601parse)
class ErrorGroupListItem:
state: ErrorGroupState
annotation: Optional[str]
annotation: str | None
errorGroupId: str
appVersion: str
appBuild: Optional[str]
appBuild: str | None
count: int
deviceCount: int
firstOccurrence: datetime.datetime
lastOccurrence: datetime.datetime
exceptionType: Optional[str]
exceptionMessage: Optional[str]
exceptionClassName: Optional[str]
exceptionClassMethod: Optional[bool]
exceptionMethod: Optional[str]
exceptionAppCode: Optional[bool]
exceptionFile: Optional[str]
exceptionLine: Optional[str]
codeRaw: Optional[str]
reasonFrames: Optional[List[HandledErrorReasonFrame]]
exceptionType: str | None
exceptionMessage: str | None
exceptionClassName: str | None
exceptionClassMethod: bool | None
exceptionMethod: str | None
exceptionAppCode: bool | None
exceptionFile: str | None
exceptionLine: str | None
codeRaw: str | None
reasonFrames: list[HandledErrorReasonFrame] | None
class ErrorGroups:
nextLink: Optional[str]
errorGroups: Optional[List[ErrorGroupListItem]]
nextLink: str | None
errorGroups: list[ErrorGroupListItem] | None
@deserialize.parser("firstOccurrence", iso8601parse)
@deserialize.parser("lastOccurrence", iso8601parse)
class ErrorGroup:
state: ErrorGroupState
annotation: Optional[str]
annotation: str | None
errorGroupId: str
appVersion: str
appBuild: Optional[str]
appBuild: str | None
count: int
deviceCount: int
firstOccurrence: datetime.datetime
lastOccurrence: datetime.datetime
exceptionType: Optional[str]
exceptionMessage: Optional[str]
exceptionClassName: Optional[str]
exceptionClassMethod: Optional[bool]
exceptionMethod: Optional[str]
exceptionAppCode: Optional[bool]
exceptionFile: Optional[str]
exceptionLine: Optional[str]
codeRaw: Optional[str]
reasonFrames: Optional[List[HandledErrorReasonFrame]]
exceptionType: str | None
exceptionMessage: str | None
exceptionClassName: str | None
exceptionClassMethod: bool | None
exceptionMethod: str | None
exceptionAppCode: bool | None
exceptionFile: str | None
exceptionLine: str | None
codeRaw: str | None
reasonFrames: list[HandledErrorReasonFrame] | None
@deserialize.parser("timestamp", iso8601parse)
class HandledError:
errorId: Optional[str]
timestamp: Optional[datetime.datetime]
deviceName: Optional[str]
osVersion: Optional[str]
osType: Optional[str]
country: Optional[str]
language: Optional[str]
userId: Optional[str]
errorId: str | None
timestamp: datetime.datetime | None
deviceName: str | None
osVersion: str | None
osType: str | None
country: str | None
language: str | None
userId: str | None
class HandledErrors:
nextLink: Optional[str]
errors: Optional[List[HandledError]]
nextLink: str | None
errors: list[HandledError] | None
@deserialize.parser("timestamp", iso8601parse)
@deserialize.parser("appLaunchTimestamp", iso8601parse)
class HandledErrorDetails:
errorId: Optional[str]
timestamp: Optional[datetime.datetime]
deviceName: Optional[str]
osVersion: Optional[str]
osType: Optional[str]
country: Optional[str]
language: Optional[str]
userId: Optional[str]
name: Optional[str]
reasonFrames: Optional[List[HandledErrorReasonFrame]]
appLaunchTimestamp: Optional[datetime.datetime]
carrierName: Optional[str]
jailbreak: Optional[bool]
properties: Optional[Dict[str, str]]
errorId: str | None
timestamp: datetime.datetime | None
deviceName: str | None
osVersion: str | None
osType: str | None
country: str | None
language: str | None
userId: str | None
name: str | None
reasonFrames: list[HandledErrorReasonFrame] | None
appLaunchTimestamp: datetime.datetime | None
carrierName: str | None
jailbreak: bool | None
properties: dict[str, str | None]
class ReleaseOrigin(enum.Enum):
@ -157,21 +157,21 @@ class ReleaseOrigin(enum.Enum):
@deserialize.auto_snake()
class BuildInfo:
branch_name: Optional[str]
commit_hash: Optional[str]
commit_message: Optional[str]
branch_name: str | None
commit_hash: str | None
commit_message: str | None
def __init__(
self,
branch_name: Optional[str] = None,
commit_hash: Optional[str] = None,
commit_message: Optional[str] = None,
branch_name: str | None = None,
commit_hash: str | None = None,
commit_message: str | None = None,
) -> None:
self.branch_name = branch_name
self.commit_hash = commit_hash
self.commit_message = commit_message
def json(self) -> Dict[str, Any]:
def json(self) -> dict[str, Any]:
result = {}
if self.branch_name is not None:
@ -203,12 +203,12 @@ class DestinationType(enum.Enum):
@deserialize.key("store_type", "type")
class Destination:
identifier: str
name: Optional[str]
is_latest: Optional[bool]
store_type: Optional[StoreType]
publishing_status: Optional[str]
destination_type: Optional[DestinationType]
display_name: Optional[str]
name: str | None
is_latest: bool | None
store_type: StoreType | None
publishing_status: str | None
destination_type: DestinationType | None
display_name: str | None
@deserialize.key("identifier", "id")
@ -216,12 +216,12 @@ class Destination:
class BasicReleaseDetailsResponse:
identifier: int
version: str
origin: Optional[ReleaseOrigin]
origin: ReleaseOrigin | None
short_version: str
enabled: bool
uploaded_at: datetime.datetime
destinations: Optional[List[Destination]]
build: Optional[BuildInfo]
destinations: list[Destination] | None
build: BuildInfo | None
class ProvisioningProfileType(enum.Enum):
@ -244,84 +244,84 @@ class ReleaseDetailsResponse:
app_display_name: str
# The app's OS.
app_os: Optional[str]
app_os: str | None
# The release's version.
version: str
# The release's origin
origin: Optional[ReleaseOrigin]
origin: ReleaseOrigin | None
# The release's short version.
short_version: str
# The release's release notes.
release_notes: Optional[str]
release_notes: str | None
# The release's provisioning profile name.
provisioning_profile_name: Optional[str]
provisioning_profile_name: str | None
# The type of the provisioning profile for the requested app version.
provisioning_profile_type: Optional[ProvisioningProfileType]
provisioning_profile_type: ProvisioningProfileType | None
# expiration date of provisioning profile in UTC format.
provisioning_profile_expiry_date: Optional[datetime.datetime]
provisioning_profile_expiry_date: datetime.datetime | None
# A flag that determines whether the release's provisioning profile is still extracted or not.
is_provisioning_profile_syncing: Optional[bool]
is_provisioning_profile_syncing: bool | None
# The release's size in bytes.
size: Optional[int]
size: int | None
# The release's minimum required operating system.
min_os: Optional[str]
min_os: str | None
# The release's device family.
device_family: Optional[str]
device_family: str | None
# The release's minimum required Android API level.
android_min_api_level: Optional[str]
android_min_api_level: str | None
# The identifier of the apps bundle.
bundle_identifier: Optional[str]
bundle_identifier: str | None
# Hashes for the packages
package_hashes: Optional[List[str]]
package_hashes: list[str | None]
# MD5 checksum of the release binary.
fingerprint: Optional[str]
fingerprint: str | None
# The uploaded time.
uploaded_at: datetime.datetime
# The URL that hosts the binary for this release.
download_url: Optional[str]
download_url: str | None
# A URL to the app's icon.
app_icon_url: Optional[str]
app_icon_url: str | None
# The href required to install a release on a mobile device. On iOS devices will be prefixed
# with itms-services://?action=download-manifest&url=
install_url: Optional[str]
install_url: str | None
destinations: Optional[List[Destination]]
destinations: list[Destination] | None
# In calls that allow passing udid in the query string, this value will hold the provisioning
# status of that UDID in this release. Will be ignored for non-iOS platforms.
is_udid_provisioned: Optional[bool]
is_udid_provisioned: bool | None
# In calls that allow passing udid in the query string, this value determines if a release can
# be re-signed. When true, after a re-sign, the tester will be able to install the release from
# his registered devices. Will not be returned for non-iOS platforms.
can_resign: Optional[bool]
can_resign: bool | None
build: Optional[BuildInfo]
build: BuildInfo | None
# This value determines the whether a release currently is enabled or disabled.
enabled: bool
# Status of the release.
status: Optional[str]
status: str | None
class ReleaseWithDistributionGroup:
@ -335,14 +335,14 @@ class ReleaseWithDistributionGroup:
class ReleaseCount:
release_id: str
distribution_group: Optional[str]
distribution_group: str | None
unique_count: int
total_count: int
class ReleaseCounts:
total: Optional[int]
counts: List[ReleaseCount]
total: int | None
counts: list[ReleaseCount]
@deserialize.key("identifier", "id")
@ -351,7 +351,7 @@ class SetUploadMetadataResponse:
error: bool
chunk_size: int
resume_restart: bool
chunk_list: List[int]
chunk_list: list[int]
blob_partitions: int
status_code: str
@ -375,7 +375,7 @@ class CreateReleaseUploadResponse:
class CommitUploadResponse:
identifier: str
upload_status: str
release_distinct_id: Optional[int]
release_distinct_id: int | None
@deserialize.key("identifier", "id")
@ -383,7 +383,7 @@ class UploadCompleteResponse:
absolute_uri: str
chunk_num: int
error: bool
error_code: Optional[str]
error_code: str | None
location: str
message: str
raw_location: str
@ -394,22 +394,20 @@ class UploadCompleteResponse:
class ReleaseDestinationResponse:
identifier: str
mandatory_update: bool
provisioning_status_url: Optional[str]
provisioning_status_url: str | None
@deserialize.key("identifier", "id")
class DestinationId:
name: Optional[str]
identifier: Optional[str]
name: str | None
identifier: str | None
def __init__(
self, *, name: Optional[str] = None, identifier: Optional[str] = None
) -> None:
def __init__(self, *, name: str | None = None, identifier: str | None = None) -> None:
self.name = name
self.identifier = identifier
def json(self) -> Dict[str, Any]:
result: Dict[str, Any] = {}
def json(self) -> dict[str, Any]:
result: dict[str, Any] = {}
if self.name is not None:
result["name"] = self.name
@ -421,20 +419,20 @@ class DestinationId:
class ReleaseUpdateRequest:
release_notes: Optional[str]
mandatory_update: Optional[bool]
destinations: Optional[List[DestinationId]]
build: Optional[BuildInfo]
notify_testers: Optional[bool]
release_notes: str | None
mandatory_update: bool | None
destinations: list[DestinationId] | None
build: BuildInfo | None
notify_testers: bool | None
def __init__(
self,
*,
release_notes: Optional[str] = None,
mandatory_update: Optional[bool] = None,
destinations: Optional[List[DestinationId]] = None,
build: Optional[BuildInfo] = None,
notify_testers: Optional[bool] = None,
release_notes: str | None = None,
mandatory_update: bool | None = None,
destinations: list[DestinationId] | None = None,
build: BuildInfo | None = None,
notify_testers: bool | None = None,
) -> None:
self.release_notes = release_notes
self.mandatory_update = mandatory_update
@ -442,8 +440,8 @@ class ReleaseUpdateRequest:
self.build = build
self.notify_testers = notify_testers
def json(self) -> Dict[str, Any]:
output: Dict[str, Any] = {}
def json(self) -> dict[str, Any]:
output: dict[str, Any] = {}
if self.release_notes is not None:
output["release_notes"] = self.release_notes
@ -452,9 +450,7 @@ class ReleaseUpdateRequest:
output["mandatory_update"] = self.mandatory_update
if self.destinations is not None:
output["destinations"] = [
destination.json() for destination in self.destinations
]
output["destinations"] = [destination.json() for destination in self.destinations]
if self.build is not None:
output["build"] = self.build.json()
@ -514,10 +510,10 @@ class User:
identifier: str
# The avatar URL of the user
avatar_url: Optional[str]
avatar_url: str | None
# User is required to send an old password in order to change the password
can_change_password: Optional[bool]
can_change_password: bool | None
# The full name of the user. Might for example be first and last name
display_name: str
@ -529,7 +525,7 @@ class User:
name: str
# The permissions the user has for the app
permissions: List[Permission]
permissions: list[Permission]
# The creation origin of this user
origin: Origin
@ -545,10 +541,10 @@ class UserToken:
description: str
# The scope the token has
scope: List[str]
scope: list[str]
# The creation date
created_at: datetime.datetime
# The value of the token - Only set when creating a new tokern
api_token: Optional[str]
api_token: str | None

Просмотреть файл

@ -5,7 +5,6 @@
import enum
import logging
from typing import List, Union
import deserialize
@ -29,7 +28,7 @@ class AppCenterTokensClient(AppCenterDerivedClient):
def __init__(self, token: str, parent_logger: logging.Logger) -> None:
super().__init__("tokens", token, parent_logger)
def get_user_tokens(self) -> List[UserToken]:
def get_user_tokens(self) -> list[UserToken]:
"""Get the users tokens
:returns: The tokens
@ -42,7 +41,7 @@ class AppCenterTokensClient(AppCenterDerivedClient):
response = self.get(request_url)
return deserialize.deserialize(List[UserToken], response.json())
return deserialize.deserialize(list[UserToken], response.json())
def create_user_token(self, name: str, scope: TokenScope) -> UserToken:
"""Create a user token.
@ -56,13 +55,11 @@ class AppCenterTokensClient(AppCenterDerivedClient):
self.log.debug(f"Creating user token name={name}, scope={scope}")
response = self.post(
request_url, data={"description": name, "scope": [scope.value]}
)
response = self.post(request_url, data={"description": name, "scope": [scope.value]})
return deserialize.deserialize(UserToken, response.json())
def delete_user_token(self, token: Union[str, UserToken]) -> None:
def delete_user_token(self, token: str | UserToken) -> None:
"""Delete a user token.
:param token: The token itself or the ID for the token

Просмотреть файл

@ -6,7 +6,7 @@
import logging
import os
import time
from typing import Iterator, List, Optional
from typing import Iterator
import urllib.parse
import deserialize
@ -56,9 +56,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
def __init__(self, token: str, parent_logger: logging.Logger) -> None:
super().__init__("versions", token, parent_logger)
def recent(
self, *, owner_name: str, app_name: str
) -> List[BasicReleaseDetailsResponse]:
def recent(self, *, owner_name: str, app_name: str) -> list[BasicReleaseDetailsResponse]:
"""Get the recent version for each distribution group.
:param str owner_name: The name of the app account owner
@ -74,9 +72,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
response = self.get(request_url)
return deserialize.deserialize(
List[BasicReleaseDetailsResponse], response.json()
)
return deserialize.deserialize(list[BasicReleaseDetailsResponse], response.json())
def all(
self,
@ -84,14 +80,14 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
owner_name: str,
app_name: str,
published_only: bool = False,
scope: Optional[str] = None,
scope: str | None = None,
) -> Iterator[BasicReleaseDetailsResponse]:
"""Get all (the 100 latest) versions.
:param str owner_name: The name of the app account owner
:param str app_name: The name of the app
:param bool published_only: Return only the published releases (defaults to false)
:param Optional[str] scope: When the scope is 'tester', only includes
:param str | None scope: When the scope is 'tester', only includes
releases that have been distributed to
groups that the user belongs to.
@ -112,9 +108,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
response = self.get(request_url)
return deserialize.deserialize(
List[BasicReleaseDetailsResponse], response.json()
)
return deserialize.deserialize(list[BasicReleaseDetailsResponse], response.json())
def release_details(
self, *, owner_name: str, app_name: str, release_id: int
@ -137,9 +131,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
return deserialize.deserialize(ReleaseDetailsResponse, response.json())
def release_id_for_version(
self, *, owner_name: str, app_name: str, version: str
) -> Optional[int]:
def release_id_for_version(self, *, owner_name: str, app_name: str, version: str) -> int | None:
"""Get the App Center release identifier for the app version (usually build number).
:param str owner_name: The name of the app account owner
@ -155,7 +147,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
return None
def latest_commit(self, *, owner_name: str, app_name: str) -> Optional[str]:
def latest_commit(self, *, owner_name: str, app_name: str) -> str | None:
"""Find the most recent release which has an available commit in it and return the commit hash.
:param str owner_name: The name of the app account owner
@ -179,9 +171,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
return None
def get_upload_url(
self, *, owner_name: str, app_name: str
) -> CreateReleaseUploadResponse:
def get_upload_url(self, *, owner_name: str, app_name: str) -> CreateReleaseUploadResponse:
"""Get the App Center release identifier for the app version (usually build number).
:param str owner_name: The name of the app account owner
@ -214,7 +204,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
*,
create_release_upload_response: CreateReleaseUploadResponse,
binary_path: str,
) -> Optional[SetUploadMetadataResponse]:
) -> SetUploadMetadataResponse | None:
"""Set the metadata for a binary upload
:param CreateReleaseUploadResponse create_release_upload_response: The response to a `get_upload_url` call
@ -230,9 +220,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
mime_type = MIME_TYPES.get(file_ext)
request_url = create_release_upload_response.upload_domain
request_url += (
f"/upload/set_metadata/{create_release_upload_response.package_asset_id}?"
)
request_url += f"/upload/set_metadata/{create_release_upload_response.package_asset_id}?"
parameters = {"file_name": file_name, "file_size": file_size}
@ -247,9 +235,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
try:
response = self.post(request_url, data={})
if response.ok:
return deserialize.deserialize(
SetUploadMetadataResponse, response.json()
)
return deserialize.deserialize(SetUploadMetadataResponse, response.json())
except Exception as ex:
if attempt < 2:
self.log.warning(f"Failed to post in set_upload_metadata: {ex}")
@ -266,7 +252,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
chunk_number: int,
chunk: bytearray,
create_release_upload_response: CreateReleaseUploadResponse,
) -> Optional[SetUploadMetadataResponse]:
) -> SetUploadMetadataResponse | None:
"""Set the metadata for a binary upload
:param CreateReleaseUploadResponse create_release_upload_response: The response to a `get_upload_url` call
@ -276,9 +262,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
"""
request_url = create_release_upload_response.upload_domain
request_url += (
f"/upload/upload_chunk/{create_release_upload_response.package_asset_id}?"
)
request_url += f"/upload/upload_chunk/{create_release_upload_response.package_asset_id}?"
parameters = {"block_number": chunk_number}
@ -303,7 +287,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
def _mark_upload_finished(
self, *, create_release_upload_response: CreateReleaseUploadResponse
) -> Optional[UploadCompleteResponse]:
) -> UploadCompleteResponse | None:
"""Mark the upload of a binary as finished
:param CreateReleaseUploadResponse create_release_upload_response: The response to a `get_upload_url` call
@ -312,9 +296,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
"""
request_url = create_release_upload_response.upload_domain
request_url += (
f"/upload/finished/{create_release_upload_response.package_asset_id}?"
)
request_url += f"/upload/finished/{create_release_upload_response.package_asset_id}?"
parameters = {"callback": ""}
@ -326,9 +308,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
try:
response = self.post_raw_data(request_url, data=None)
if response.ok:
return deserialize.deserialize(
UploadCompleteResponse, response.json()
)
return deserialize.deserialize(UploadCompleteResponse, response.json())
except Exception as ex:
if attempt < 2:
self.log.warning(f"Failed to post in _mark_upload_finished: {ex}")
@ -375,9 +355,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
create_release_upload_response=create_release_upload_response,
)
if response is None:
self.log.warn(
f"Failed to get response for uploading chunk {chunk_number}"
)
self.log.warn(f"Failed to get response for uploading chunk {chunk_number}")
unhandled_chunks.append((chunk_number, 0, chunk))
except Exception as ex:
self.log.warn(
@ -397,9 +375,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
return False
direct_upload_chunk(chunk, chunk_number)
self._mark_upload_finished(
create_release_upload_response=create_release_upload_response
)
self._mark_upload_finished(create_release_upload_response=create_release_upload_response)
return True
@ -462,9 +438,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
continue
try:
response_data = deserialize.deserialize(
CommitUploadResponse, response.json()
)
response_data = deserialize.deserialize(CommitUploadResponse, response.json())
except Exception as ex:
self.log.warning(f"Failed to get response data: {ex}")
wait()
@ -580,19 +554,19 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
app_name: str,
binary_path: str,
release_notes: str,
branch_name: Optional[str] = None,
commit_hash: Optional[str] = None,
commit_message: Optional[str] = None,
) -> Optional[int]:
branch_name: str | None = None,
commit_hash: str | None = None,
commit_message: str | None = None,
) -> int | None:
"""Get the App Center release identifier for the app version (usually build number).
:param str owner_name: The name of the app account owner
:param str app_name: The name of the app
:param str binary_path: The path to the binary to upload
:param str release_notes: The release notes for the release
:param Optional[str] branch_name: The git branch that the build came from
:param Optional[str] commit_hash: The hash of the commit that was just built
:param Optional[str] commit_message: The message of the commit that was just built
:param str | None branch_name: The git branch that the build came from
:param str | None commit_hash: The hash of the commit that was just built
:param str | None commit_message: The message of the commit that was just built
:raises FileNotFoundError: If the supplied binary is not found
:raises Exception: If we don't get a release ID back after upload
@ -635,9 +609,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
commit_hash=commit_hash,
commit_message=commit_message,
)
update_request = ReleaseUpdateRequest(
release_notes=release_notes, build=build_info
)
update_request = ReleaseUpdateRequest(release_notes=release_notes, build=build_info)
self.update_release(
owner_name=owner_name,
@ -648,6 +620,7 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
return upload_end_response.release_distinct_id
# pylint: disable=too-many-arguments
def upload_and_release(
self,
*,
@ -656,10 +629,10 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
binary_path: str,
group_id: str,
release_notes: str,
notify_testers: Optional[bool] = None,
branch_name: Optional[str] = None,
commit_hash: Optional[str] = None,
commit_message: Optional[str] = None,
notify_testers: bool | None = None,
branch_name: str | None = None,
commit_hash: str | None = None,
commit_message: str | None = None,
) -> ReleaseDetailsResponse:
"""Get the App Center release identifier for the app version (usually build number).
@ -668,10 +641,10 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
:param str binary_path: The path to the binary to upload
:param str group_id: The ID of the group to release to
:param str release_notes: The release notes for the release
:param Optional[bool] notify_testers: Set to True to notify testers about this build
:param Optional[str] branch_name: The git branch that the build came from
:param Optional[str] commit_hash: The hash of the commit that was just built
:param Optional[str] commit_message: The message of the commit that was just built
:param bool | None notify_testers: Set to True to notify testers about this build
:param str | None branch_name: The git branch that the build came from
:param str | None commit_hash: The hash of the commit that was just built
:param str | None commit_message: The message of the commit that was just built
:raises FileNotFoundError: If the supplied binary is not found
:raises Exception: If we don't get a release ID back after upload
@ -700,6 +673,6 @@ class AppCenterVersionsClient(AppCenterDerivedClient):
notify_testers=notify_testers if notify_testers else False,
)
return self.release_details(
owner_name=owner_name, app_name=app_name, release_id=release_id
)
return self.release_details(owner_name=owner_name, app_name=app_name, release_id=release_id)
# pylint: enable=too-many-arguments

910
poetry.lock сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -20,16 +20,16 @@ classifiers = [
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Software Development',
'Topic :: Utilities'
]
[tool.poetry.dependencies]
python = "^3.8"
python = "^3.9"
azure-storage-blob = "^2.1.0"
deserialize = "^2.0.1"
requests = "^2.31.0"
@ -37,11 +37,11 @@ tenacity = "^8.2.2"
types-requests = "^2.31.0.2"
[tool.poetry.dev-dependencies]
black = "^23.7.0"
mypy = "^1.4.1"
pylint = "^2.17.5"
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
black = "24.4.2"
mypy = "1.10.0"
pylint = "3.1.0"
pytest = "^8.2.0"
pytest-cov = "^5.0.0"
[[tool.mypy.overrides]]
module = [

Просмотреть файл

@ -9,7 +9,6 @@ import os
import re
import subprocess
import sys
from typing import List, Optional
import uuid
import pytest
@ -22,7 +21,7 @@ import appcenter
# pylint: disable=redefined-outer-name
def get_from_keychain() -> Optional[str]:
def get_from_keychain() -> str | None:
"""Get the test details from the keychain.
:returns: A string with the details (colon separated)
@ -44,9 +43,7 @@ def get_from_keychain() -> Optional[str]:
return None
# The output is somewhat complex. We are looking for the line starting "password:"
password_lines = [
line for line in output.split("\n") if line.startswith("password: ")
]
password_lines = [line for line in output.split("\n") if line.startswith("password: ")]
if len(password_lines) != 1:
raise Exception("Failed to get password from security output")
@ -71,7 +68,7 @@ def get_from_keychain() -> Optional[str]:
return password
def get_tokens() -> List[str]:
def get_tokens() -> list[str]:
"""Get the tokens for authentication.
:returns: The owner name, app name and token as a tuple.
@ -212,9 +209,7 @@ def test_release_details(owner_name: str, app_name: str, token: str):
def test_latest_commit(owner_name: str, app_name: str, token: str):
"""Test release details."""
client = appcenter.AppCenterClient(access_token=token)
commit_hash = client.versions.latest_commit(
owner_name=owner_name, app_name=app_name
)
commit_hash = client.versions.latest_commit(owner_name=owner_name, app_name=app_name)
assert commit_hash is not None