diff --git a/box_sdk_gen/ccg_auth.py b/box_sdk_gen/ccg_auth.py index 0b0a1cd..7f7d56f 100644 --- a/box_sdk_gen/ccg_auth.py +++ b/box_sdk_gen/ccg_auth.py @@ -3,6 +3,7 @@ from typing import Union, Optional from .auth import Authentication +from .token_storage import TokenStorage, InMemoryTokenStorage from .auth_schemas import ( TokenRequestBoxSubjectType, TokenRequest, @@ -20,6 +21,7 @@ def __init__( client_secret: str, enterprise_id: Union[None, str] = None, user_id: Union[None, str] = None, + token_storage: TokenStorage = None, ): """ :param client_id: @@ -45,7 +47,12 @@ def __init__( + :param token_storage: + Object responsible for storing token. If no custom implementation provided, + the token will be stored in memory. """ + if token_storage is None: + token_storage = InMemoryTokenStorage() if not enterprise_id and not user_id: raise Exception("Enterprise ID or User ID is needed") @@ -53,6 +60,7 @@ def __init__( self.client_secret = client_secret self.enterprise_id = enterprise_id self.user_id = user_id + self.token_storage = token_storage class CCGAuth(Authentication): @@ -62,7 +70,7 @@ def __init__(self, config: CCGConfig): Configuration object of Client Credentials Grant auth. """ self.config = config - self.token: Union[None, AccessToken] = None + self.token_storage = config.token_storage if config.user_id: self.subject_id = self.config.user_id @@ -79,9 +87,10 @@ def retrieve_token( :param network_session: An object to keep network session state :return: Access token """ - if self.token is None: - self.refresh_token(network_session=network_session) - return self.token + token = self.token_storage.get() + if token is None: + return self.refresh_token(network_session=network_session) + return token def refresh_token( self, network_session: Optional[NetworkSession] = None @@ -109,9 +118,9 @@ def refresh_token( ), ) - token_response = AccessToken.from_dict(json.loads(response.text)) - self.token = token_response - return token_response + new_token = AccessToken.from_dict(json.loads(response.text)) + self.token_storage.store(new_token) + return new_token def as_user(self, user_id: str): """ @@ -129,7 +138,7 @@ def as_user(self, user_id: str): """ self.subject_id = user_id self.subject_type = TokenRequestBoxSubjectType.USER - self.token = None + self.token_storage.clear() def as_enterprise(self, enterprise_id: str): """ @@ -140,4 +149,4 @@ def as_enterprise(self, enterprise_id: str): """ self.subject_id = enterprise_id self.subject_type = TokenRequestBoxSubjectType.ENTERPRISE - self.token = None + self.token_storage.clear() diff --git a/box_sdk_gen/fetch.py b/box_sdk_gen/fetch.py index ea6176f..9121e8d 100644 --- a/box_sdk_gen/fetch.py +++ b/box_sdk_gen/fetch.py @@ -42,6 +42,7 @@ class FetchOptions: params: Dict[str, str] = None headers: Dict[str, str] = None body: str = None + file_stream: ByteStream = None multipart_data: List[MultipartItem] = None content_type: str = "" response_format: Optional[str] = None @@ -100,7 +101,7 @@ def fetch(url: str, options: FetchOptions) -> FetchResponse: method=options.method, url=url, headers=headers, - body=options.body, + body=options.file_stream or options.body, content_type=options.content_type, params=params, multipart_data=options.multipart_data, diff --git a/box_sdk_gen/jwt_auth.py b/box_sdk_gen/jwt_auth.py index e7f492d..b1437c8 100644 --- a/box_sdk_gen/jwt_auth.py +++ b/box_sdk_gen/jwt_auth.py @@ -14,6 +14,7 @@ jwt, default_backend, serialization = None, None, None from .auth import Authentication +from .token_storage import TokenStorage, InMemoryTokenStorage from .auth_schemas import ( TokenRequestBoxSubjectType, TokenRequest, @@ -35,6 +36,7 @@ def __init__( enterprise_id: Optional[str] = None, user_id: Optional[str] = None, jwt_algorithm: str = 'RS256', + token_storage: TokenStorage = None, **_kwargs ): """ @@ -69,7 +71,12 @@ def __init__( :param jwt_algorithm: Which algorithm to use for signing the JWT assertion. Must be one of 'RS256', 'RS384', 'RS512'. + :param token_storage: + Object responsible for storing token. If no custom implementation provided, + the token will be stored in memory. """ + if token_storage is None: + token_storage = InMemoryTokenStorage() if not enterprise_id and not user_id: raise Exception("Enterprise ID or User ID is needed") @@ -81,10 +88,11 @@ def __init__( self.private_key = private_key self.private_key_passphrase = private_key_passphrase self.jwt_algorithm = jwt_algorithm + self.token_storage = token_storage @classmethod def from_config_json_string( - cls, config_json_string: str, **kwargs: Any + cls, config_json_string: str, token_storage: TokenStorage = None, **kwargs: Any ) -> 'JWTConfig': """ Create an auth instance as defined by a string content of JSON file downloaded from the Box Developer Console. @@ -92,6 +100,9 @@ def from_config_json_string( :param config_json_string: String content of JSON file containing the configuration. + :param token_storage: + Object responsible for storing token. If no custom implementation provided, + the token will be stored in memory. :return: Auth instance configured as specified by the config dictionary. """ @@ -111,22 +122,30 @@ def from_config_json_string( private_key_passphrase=config_dict['boxAppSettings']['appAuth'].get( 'passphrase', None ), + token_storage=token_storage, **kwargs ) @classmethod - def from_config_file(cls, config_file_path: str, **kwargs: Any) -> 'JWTConfig': + def from_config_file( + cls, config_file_path: str, token_storage: TokenStorage = None, **kwargs: Any + ) -> 'JWTConfig': """ Create an auth instance as defined by a JSON file downloaded from the Box Developer Console. See https://developer.box.com/en/guides/authentication/jwt/ for more information. :param config_file_path: Path to the JSON file containing the configuration. + :param token_storage: + Object responsible for storing token. If no custom implementation provided, + the token will be stored in memory. :return: Auth instance configured as specified by the JSON file. """ with open(config_file_path, encoding='utf-8') as config_file: - return cls.from_config_json_string(config_file.read(), **kwargs) + return cls.from_config_json_string( + config_file.read(), token_storage, **kwargs + ) class JWTAuth(Authentication): @@ -142,7 +161,7 @@ def __init__(self, config: JWTConfig): ) self.config = config - self.token: Union[None, AccessToken] = None + self.token_storage = config.token_storage if config.enterprise_id: self.subject_type = TokenRequestBoxSubjectType.ENTERPRISE @@ -163,9 +182,10 @@ def retrieve_token( :param network_session: An object to keep network session state :return: Access token """ - if self.token is None: - self.refresh_token(network_session=network_session) - return self.token + token = self.token_storage.get() + if token is None: + return self.refresh_token(network_session=network_session) + return token def refresh_token( self, network_session: Optional[NetworkSession] = None @@ -218,9 +238,9 @@ def refresh_token( ), ) - token_response = AccessToken.from_dict(json.loads(response.text)) - self.token = token_response - return self.token + new_token = AccessToken.from_dict(json.loads(response.text)) + self.token_storage.store(new_token) + return new_token def as_user(self, user_id: str): """ @@ -238,7 +258,7 @@ def as_user(self, user_id: str): """ self.subject_id = user_id self.subject_type = TokenRequestBoxSubjectType.USER - self.token = None + self.token_storage.clear() def as_enterprise(self, enterprise_id: str): """ @@ -249,7 +269,7 @@ def as_enterprise(self, enterprise_id: str): """ self.subject_id = enterprise_id self.subject_type = TokenRequestBoxSubjectType.ENTERPRISE - self.token = None + self.token_storage.clear() @classmethod def _get_rsa_private_key( diff --git a/box_sdk_gen/managers/chunked_uploads.py b/box_sdk_gen/managers/chunked_uploads.py index db766d1..56b9f69 100644 --- a/box_sdk_gen/managers/chunked_uploads.py +++ b/box_sdk_gen/managers/chunked_uploads.py @@ -1,13 +1,19 @@ +from typing import List + from typing import Optional from typing import Dict import json -from typing import List - from box_sdk_gen.base_object import BaseObject +from box_sdk_gen.utils import Buffer + +from box_sdk_gen.utils import HashName + +from box_sdk_gen.utils import Iterator + from box_sdk_gen.schemas import UploadSession from box_sdk_gen.schemas import ClientError @@ -36,6 +42,38 @@ from box_sdk_gen.fetch import FetchResponse +from box_sdk_gen.utils import generate_byte_stream_from_buffer + +from box_sdk_gen.utils import hex_to_base_64 + +from box_sdk_gen.utils import iterate_chunks + +from box_sdk_gen.utils import read_byte_stream + +from box_sdk_gen.utils import reduce_iterator + +from box_sdk_gen.utils import Hash + +from box_sdk_gen.utils import list_concat + +from box_sdk_gen.utils import buffer_length + + +class PartAccumulator: + def __init__( + self, + last_index: int, + parts: List[UploadPart], + file_size: int, + upload_session_id: str, + file_hash: Hash, + ): + self.last_index = last_index + self.parts = parts + self.file_size = file_size + self.upload_session_id = upload_session_id + self.file_hash = file_hash + class ChunkedUploadsManager: def __init__( @@ -218,7 +256,7 @@ def upload_file_part( FetchOptions( method='PUT', headers=headers_map, - body=request_body, + file_stream=request_body, content_type='application/octet-stream', response_format='json', auth=self.auth, @@ -384,3 +422,95 @@ def create_file_upload_session_commit( ), ) return Files.from_dict(json.loads(response.text)) + + def reducer(self, acc: PartAccumulator, chunk: ByteStream): + last_index: int = acc.last_index + parts: List[UploadPart] = acc.parts + chunk_buffer: Buffer = read_byte_stream(chunk) + hash: Hash = Hash(algorithm=HashName.SHA1.value) + hash.update_hash(chunk_buffer) + sha_1: str = hash.digest_hash('base64') + digest: str = ''.join(['sha=', sha_1]) + chunk_size: int = buffer_length(chunk_buffer) + bytes_start: int = last_index + 1 + bytes_end: int = last_index + chunk_size + content_range: str = ''.join( + [ + 'bytes ', + to_string(bytes_start), + '-', + to_string(bytes_end), + '/', + to_string(acc.file_size), + ] + ) + uploaded_part: UploadedPart = self.upload_file_part( + upload_session_id=acc.upload_session_id, + request_body=generate_byte_stream_from_buffer(chunk_buffer), + digest=digest, + content_range=content_range, + ) + part: UploadPart = uploaded_part.part + part_sha_1: str = hex_to_base_64(part.sha_1) + assert part_sha_1 == sha_1 + assert part.size == chunk_size + assert part.offset == bytes_start + acc.file_hash.update_hash(chunk_buffer) + return PartAccumulator( + last_index=bytes_end, + parts=list_concat(parts, [part]), + file_size=acc.file_size, + upload_session_id=acc.upload_session_id, + file_hash=acc.file_hash, + ) + + def upload_big_file( + self, file: ByteStream, file_name: str, file_size: int, parent_folder_id: str + ): + """ + Starts the process of chunk uploading a big file. Should return a File object representing uploaded file. + :param file: The stream of the file to upload. + :type file: ByteStream + :param file_name: The name of the file, which will be used for storage in Box. + :type file_name: str + :param file_size: The total size of the file for the chunked upload in bytes. + :type file_size: int + :param parent_folder_id: The ID of the folder where the file should be uploaded. + :type parent_folder_id: str + """ + upload_session: UploadSession = self.create_file_upload_session( + folder_id=parent_folder_id, file_size=file_size, file_name=file_name + ) + upload_session_id: str = upload_session.id + part_size: int = upload_session.part_size + total_parts: int = upload_session.total_parts + assert part_size * total_parts >= file_size + assert upload_session.num_parts_processed == 0 + file_hash: Hash = Hash(algorithm=HashName.SHA1.value) + chunks_iterator: Iterator = iterate_chunks(file, part_size) + results: PartAccumulator = reduce_iterator( + chunks_iterator, + self.reducer, + PartAccumulator( + last_index=-1, + parts=[], + file_size=file_size, + upload_session_id=upload_session_id, + file_hash=file_hash, + ), + ) + parts: List[UploadPart] = results.parts + processed_session_parts: UploadParts = self.get_file_upload_session_parts( + upload_session_id=upload_session_id + ) + assert processed_session_parts.total_count == total_parts + processed_session: UploadSession = self.get_file_upload_session_by_id( + upload_session_id=upload_session_id + ) + assert processed_session.num_parts_processed == total_parts + sha_1: str = file_hash.digest_hash('base64') + digest: str = ''.join(['sha=', sha_1]) + committed_session: Files = self.create_file_upload_session_commit( + upload_session_id=upload_session_id, parts=parts, digest=digest + ) + return committed_session.entries[0] diff --git a/box_sdk_gen/managers/file_requests.py b/box_sdk_gen/managers/file_requests.py index ed2e204..7d44513 100644 --- a/box_sdk_gen/managers/file_requests.py +++ b/box_sdk_gen/managers/file_requests.py @@ -8,6 +8,8 @@ import json +from box_sdk_gen.base_object import BaseObject + from box_sdk_gen.schemas import FileRequest from box_sdk_gen.schemas import ClientError @@ -178,7 +180,7 @@ def update_file_request_by_id( """ if extra_headers is None: extra_headers = {} - request_body = FileRequestUpdateRequest( + request_body = BaseObject( title=title, description=description, status=status, @@ -301,7 +303,7 @@ def create_file_request_copy( """ if extra_headers is None: extra_headers = {} - request_body = FileRequestCopyRequest( + request_body = BaseObject( folder=folder, title=title, description=description, diff --git a/box_sdk_gen/managers/folder_metadata.py b/box_sdk_gen/managers/folder_metadata.py index a17efc3..07ccd77 100644 --- a/box_sdk_gen/managers/folder_metadata.py +++ b/box_sdk_gen/managers/folder_metadata.py @@ -14,6 +14,8 @@ from box_sdk_gen.schemas import ClientError +from box_sdk_gen.schemas import MetadataFull + from box_sdk_gen.schemas import Metadata from box_sdk_gen.auth import Authentication @@ -166,7 +168,7 @@ def get_folder_metadata_by_id( scope: GetFolderMetadataByIdScopeArg, template_key: str, extra_headers: Optional[Dict[str, Optional[str]]] = None, - ) -> Metadata: + ) -> MetadataFull: """ Retrieves the instance of a metadata template that has been applied to a @@ -213,7 +215,7 @@ def get_folder_metadata_by_id( network_session=self.network_session, ), ) - return Metadata.from_dict(json.loads(response.text)) + return MetadataFull.from_dict(json.loads(response.text)) def create_folder_metadata_by_id( self, diff --git a/box_sdk_gen/managers/integration_mappings.py b/box_sdk_gen/managers/integration_mappings.py index fb45edc..f8c469a 100644 --- a/box_sdk_gen/managers/integration_mappings.py +++ b/box_sdk_gen/managers/integration_mappings.py @@ -146,7 +146,7 @@ def create_integration_mapping_slack( """ if extra_headers is None: extra_headers = {} - request_body = IntegrationMappingSlackCreateRequest( + request_body = BaseObject( partner_item=partner_item, box_item=box_item, options=options ) headers_map: Dict[str, str] = prepare_params({**extra_headers}) diff --git a/box_sdk_gen/managers/search.py b/box_sdk_gen/managers/search.py index 531c932..cc48d84 100644 --- a/box_sdk_gen/managers/search.py +++ b/box_sdk_gen/managers/search.py @@ -10,6 +10,8 @@ import json +from box_sdk_gen.base_object import BaseObject + from box_sdk_gen.schemas import MetadataQueryResults from box_sdk_gen.schemas import ClientError @@ -189,7 +191,7 @@ def create_metadata_query_execute_read( """ if extra_headers is None: extra_headers = {} - request_body = MetadataQuery( + request_body = BaseObject( from_=from_, query=query, query_params=query_params, diff --git a/box_sdk_gen/managers/shield_information_barrier_reports.py b/box_sdk_gen/managers/shield_information_barrier_reports.py index c0c84bf..6a549d5 100644 --- a/box_sdk_gen/managers/shield_information_barrier_reports.py +++ b/box_sdk_gen/managers/shield_information_barrier_reports.py @@ -6,6 +6,8 @@ from box_sdk_gen.schemas import ShieldInformationBarrierBase +from box_sdk_gen.base_object import BaseObject + from box_sdk_gen.schemas import ShieldInformationBarrierReport from box_sdk_gen.schemas import ClientError @@ -95,9 +97,7 @@ def create_shield_information_barrier_report( """ if extra_headers is None: extra_headers = {} - request_body = ShieldInformationBarrierReference( - shield_information_barrier=shield_information_barrier - ) + request_body = BaseObject(shield_information_barrier=shield_information_barrier) headers_map: Dict[str, str] = prepare_params({**extra_headers}) response: FetchResponse = fetch( ''.join(['https://api.box.com/2.0/shield_information_barrier_reports']), diff --git a/box_sdk_gen/managers/shield_information_barriers.py b/box_sdk_gen/managers/shield_information_barriers.py index 8fda88e..d2ff7dd 100644 --- a/box_sdk_gen/managers/shield_information_barriers.py +++ b/box_sdk_gen/managers/shield_information_barriers.py @@ -206,7 +206,7 @@ def create_shield_information_barrier( """ if extra_headers is None: extra_headers = {} - request_body = ShieldInformationBarrier( + request_body = BaseObject( id=id, type=type, enterprise=enterprise, diff --git a/box_sdk_gen/managers/sign_requests.py b/box_sdk_gen/managers/sign_requests.py index 15661f7..fe6bcec 100644 --- a/box_sdk_gen/managers/sign_requests.py +++ b/box_sdk_gen/managers/sign_requests.py @@ -14,6 +14,8 @@ from box_sdk_gen.schemas import SignRequestPrefillTag +from box_sdk_gen.base_object import BaseObject + from box_sdk_gen.schemas import SignRequest from box_sdk_gen.schemas import ClientError @@ -236,7 +238,7 @@ def create_sign_request( """ if extra_headers is None: extra_headers = {} - request_body = SignRequestCreateRequest( + request_body = BaseObject( source_files=source_files, signers=signers, is_document_preparation_needed=is_document_preparation_needed, diff --git a/box_sdk_gen/managers/zip_downloads.py b/box_sdk_gen/managers/zip_downloads.py index 16c4439..07b75fa 100644 --- a/box_sdk_gen/managers/zip_downloads.py +++ b/box_sdk_gen/managers/zip_downloads.py @@ -10,6 +10,8 @@ import json +from box_sdk_gen.base_object import BaseObject + from box_sdk_gen.schemas import ZipDownload from box_sdk_gen.schemas import ClientError @@ -121,9 +123,7 @@ def create_zip_download( """ if extra_headers is None: extra_headers = {} - request_body = ZipDownloadRequest( - items=items, download_file_name=download_file_name - ) + request_body = BaseObject(items=items, download_file_name=download_file_name) headers_map: Dict[str, str] = prepare_params({**extra_headers}) response: FetchResponse = fetch( ''.join(['https://api.box.com/2.0/zip_downloads']), diff --git a/box_sdk_gen/oauth.py b/box_sdk_gen/oauth.py index dcbfb0b..492a3b4 100644 --- a/box_sdk_gen/oauth.py +++ b/box_sdk_gen/oauth.py @@ -3,6 +3,7 @@ from typing import Optional from .auth import Authentication +from .token_storage import TokenStorage, InMemoryTokenStorage from .auth_schemas import TokenRequest, TokenRequestGrantType from .fetch import fetch, FetchResponse, FetchOptions from .network import NetworkSession @@ -11,18 +12,23 @@ class OAuthConfig: def __init__( - self, - client_id: str, - client_secret: str, + self, client_id: str, client_secret: str, token_storage: TokenStorage = None ): """ :param client_id: Box API key used for identifying the application the user is authenticating with. :param client_secret: Box API secret used for making auth requests. + :param token_storage: + Object responsible for storing token. If no custom implementation provided, + the token will be stored in memory. """ + + if token_storage is None: + token_storage = InMemoryTokenStorage() self.client_id = client_id self.client_secret = client_secret + self.token_storage = token_storage class GetAuthorizeUrlOptions: @@ -59,7 +65,7 @@ def __init__(self, config: OAuthConfig): Configuration object of OAuth. """ self.config = config - self.token: Optional[AccessToken] = None + self.token_storage = config.token_storage def get_authorize_url( self, options: Optional[GetAuthorizeUrlOptions] = None @@ -104,7 +110,7 @@ def get_authorize_url( def get_tokens_authorization_code_grant( self, authorization_code: str, network_session: Optional[NetworkSession] = None - ) -> str: + ) -> AccessToken: """ Send token request and return the access_token :param authorization_code: Short-lived authorization code @@ -118,8 +124,9 @@ def get_tokens_authorization_code_grant( code=authorization_code, ) - self.token = self._send_token_request(request_body, network_session) - return self.token.access_token + token: AccessToken = self._send_token_request(request_body, network_session) + self.token_storage.store(token) + return token def retrieve_token( self, network_session: Optional[NetworkSession] = None @@ -129,12 +136,13 @@ def retrieve_token( :param network_session: An object to keep network session state :return: Valid access token """ - if self.token is None: + token = self.token_storage.get() + if token is None: raise Exception( "Access and refresh tokens not available. Authenticate before making" " any API call first." ) - return self.token + return token def refresh_token( self, @@ -147,15 +155,24 @@ def refresh_token( :param refresh_token: Refresh token, which can be used to obtain a new access token :return: Valid access token """ + old_token: Optional[AccessToken] = self.token_storage.get() + token_used_for_refresh = ( + refresh_token or old_token.refresh_token if old_token else None + ) + + if token_used_for_refresh is None: + raise Exception("No refresh_token is available.") + request_body = TokenRequest( grant_type=TokenRequestGrantType.REFRESH_TOKEN, client_id=self.config.client_id, client_secret=self.config.client_secret, - refresh_token=refresh_token or self.token.refresh_token, + refresh_token=refresh_token or old_token.refresh_token, ) - self.token = self._send_token_request(request_body, network_session) - return self.token + new_token = self._send_token_request(request_body, network_session) + self.token_storage.store(new_token) + return new_token @staticmethod def _send_token_request( diff --git a/box_sdk_gen/schemas.py b/box_sdk_gen/schemas.py index 2c07a87..79c421f 100644 --- a/box_sdk_gen/schemas.py +++ b/box_sdk_gen/schemas.py @@ -976,124 +976,6 @@ def __init__( self.entries = entries -class CollaborationAllowlistExemptTargetTypeField(str, Enum): - COLLABORATION_WHITELIST = 'collaboration_whitelist' - - -class CollaborationAllowlistExemptTargetEnterpriseFieldTypeField(str, Enum): - ENTERPRISE = 'enterprise' - - -class CollaborationAllowlistExemptTargetEnterpriseField(BaseObject): - def __init__( - self, - id: Optional[str] = None, - type: Optional[ - CollaborationAllowlistExemptTargetEnterpriseFieldTypeField - ] = None, - name: Optional[str] = None, - **kwargs - ): - """ - :param id: The unique identifier for this enterprise. - :type id: Optional[str], optional - :param type: `enterprise` - :type type: Optional[CollaborationAllowlistExemptTargetEnterpriseFieldTypeField], optional - :param name: The name of the enterprise - :type name: Optional[str], optional - """ - super().__init__(**kwargs) - self.id = id - self.type = type - self.name = name - - -class CollaborationAllowlistExemptTargetUserFieldTypeField(str, Enum): - ENTERPRISE = 'enterprise' - - -class CollaborationAllowlistExemptTargetUserField(BaseObject): - def __init__( - self, - id: Optional[str] = None, - type: Optional[CollaborationAllowlistExemptTargetUserFieldTypeField] = None, - name: Optional[str] = None, - **kwargs - ): - """ - :param id: The unique identifier for this enterprise. - :type id: Optional[str], optional - :param type: `enterprise` - :type type: Optional[CollaborationAllowlistExemptTargetUserFieldTypeField], optional - :param name: The name of the enterprise - :type name: Optional[str], optional - """ - super().__init__(**kwargs) - self.id = id - self.type = type - self.name = name - - -class CollaborationAllowlistExemptTarget(BaseObject): - def __init__( - self, - id: Optional[str] = None, - type: Optional[CollaborationAllowlistExemptTargetTypeField] = None, - enterprise: Optional[CollaborationAllowlistExemptTargetEnterpriseField] = None, - user: Optional[CollaborationAllowlistExemptTargetUserField] = None, - created_at: Optional[str] = None, - modified_at: Optional[str] = None, - **kwargs - ): - """ - :param id: The unique identifier for this exemption - :type id: Optional[str], optional - :param type: `collaboration_whitelist` - :type type: Optional[CollaborationAllowlistExemptTargetTypeField], optional - :param created_at: The time the entry was created - :type created_at: Optional[str], optional - :param modified_at: The time the entry was modified - :type modified_at: Optional[str], optional - """ - super().__init__(**kwargs) - self.id = id - self.type = type - self.enterprise = enterprise - self.user = user - self.created_at = created_at - self.modified_at = modified_at - - -class CollaborationAllowlistExemptTargets(BaseObject): - def __init__( - self, - limit: Optional[int] = None, - next_marker: Optional[int] = None, - prev_marker: Optional[int] = None, - entries: Optional[List[CollaborationAllowlistExemptTarget]] = None, - **kwargs - ): - """ - :param limit: The limit that was used for these entries. This will be the same as the - `limit` query parameter unless that value exceeded the maximum value - allowed. The maximum value varies by API. - :type limit: Optional[int], optional - :param next_marker: The marker for the start of the next page of results. - :type next_marker: Optional[int], optional - :param prev_marker: The marker for the start of the previous page of results. - :type prev_marker: Optional[int], optional - :param entries: A list of users exempt from any of the restrictions - imposed by the list of allowed collaboration domains - for this enterprise. - :type entries: Optional[List[CollaborationAllowlistExemptTarget]], optional - """ - super().__init__(**kwargs) - self.limit = limit - self.next_marker = next_marker - self.prev_marker = prev_marker - self.entries = entries - - class CollectionTypeField(str, Enum): COLLECTION = 'collection' @@ -3614,38 +3496,38 @@ class UserBaseTypeField(str, Enum): class UserBase(BaseObject): - def __init__(self, type: UserBaseTypeField, id: Optional[str] = None, **kwargs): + def __init__(self, id: str, type: UserBaseTypeField, **kwargs): """ + :param id: The unique identifier for this user + :type id: str :param type: `user` :type type: UserBaseTypeField - :param id: The unique identifier for this user - :type id: Optional[str], optional """ super().__init__(**kwargs) - self.type = type self.id = id + self.type = type class UserIntegrationMappings(UserBase): def __init__( self, + id: str, type: UserBaseTypeField, name: Optional[str] = None, login: Optional[str] = None, - id: Optional[str] = None, **kwargs ): """ + :param id: The unique identifier for this user + :type id: str :param type: `user` :type type: UserBaseTypeField :param name: The display name of this user :type name: Optional[str], optional :param login: The primary email address of this user :type login: Optional[str], optional - :param id: The unique identifier for this user - :type id: Optional[str], optional """ - super().__init__(type=type, id=id, **kwargs) + super().__init__(id=id, type=type, **kwargs) self.name = name self.login = login @@ -3653,23 +3535,23 @@ def __init__( class UserCollaborations(UserBase): def __init__( self, + id: str, type: UserBaseTypeField, name: Optional[str] = None, login: Optional[str] = None, - id: Optional[str] = None, **kwargs ): """ + :param id: The unique identifier for this user + :type id: str :param type: `user` :type type: UserBaseTypeField :param name: The display name of this user. If the collaboration status is `pending`, an empty string is returned. :type name: Optional[str], optional :param login: The primary email address of this user. If the collaboration status is `pending`, an empty string is returned. :type login: Optional[str], optional - :param id: The unique identifier for this user - :type id: Optional[str], optional """ - super().__init__(type=type, id=id, **kwargs) + super().__init__(id=id, type=type, **kwargs) self.name = name self.login = login @@ -3677,23 +3559,23 @@ def __init__( class UserMini(UserBase): def __init__( self, + id: str, type: UserBaseTypeField, name: Optional[str] = None, login: Optional[str] = None, - id: Optional[str] = None, **kwargs ): """ + :param id: The unique identifier for this user + :type id: str :param type: `user` :type type: UserBaseTypeField :param name: The display name of this user :type name: Optional[str], optional :param login: The primary email address of this user :type login: Optional[str], optional - :param id: The unique identifier for this user - :type id: Optional[str], optional """ - super().__init__(type=type, id=id, **kwargs) + super().__init__(id=id, type=type, **kwargs) self.name = name self.login = login @@ -3772,6 +3654,7 @@ def __init__( class User(UserMini): def __init__( self, + id: str, type: UserBaseTypeField, created_at: Optional[str] = None, modified_at: Optional[str] = None, @@ -3788,10 +3671,11 @@ def __init__( notification_email: Optional[UserNotificationEmailField] = None, name: Optional[str] = None, login: Optional[str] = None, - id: Optional[str] = None, **kwargs ): """ + :param id: The unique identifier for this user + :type id: str :param type: `user` :type type: UserBaseTypeField :param created_at: When the user object was created @@ -3828,10 +3712,8 @@ def __init__( :type name: Optional[str], optional :param login: The primary email address of this user :type login: Optional[str], optional - :param id: The unique identifier for this user - :type id: Optional[str], optional """ - super().__init__(type=type, name=name, login=login, id=id, **kwargs) + super().__init__(id=id, type=type, name=name, login=login, **kwargs) self.created_at = created_at self.modified_at = modified_at self.language = language @@ -6834,6 +6716,98 @@ def __init__( self.entries = entries +class CollaborationAllowlistExemptTargetTypeField(str, Enum): + COLLABORATION_WHITELIST_EXEMPT_TARGET = 'collaboration_whitelist_exempt_target' + + +class CollaborationAllowlistExemptTargetEnterpriseFieldTypeField(str, Enum): + ENTERPRISE = 'enterprise' + + +class CollaborationAllowlistExemptTargetEnterpriseField(BaseObject): + def __init__( + self, + id: Optional[str] = None, + type: Optional[ + CollaborationAllowlistExemptTargetEnterpriseFieldTypeField + ] = None, + name: Optional[str] = None, + **kwargs + ): + """ + :param id: The unique identifier for this enterprise. + :type id: Optional[str], optional + :param type: `enterprise` + :type type: Optional[CollaborationAllowlistExemptTargetEnterpriseFieldTypeField], optional + :param name: The name of the enterprise + :type name: Optional[str], optional + """ + super().__init__(**kwargs) + self.id = id + self.type = type + self.name = name + + +class CollaborationAllowlistExemptTarget(BaseObject): + def __init__( + self, + id: Optional[str] = None, + type: Optional[CollaborationAllowlistExemptTargetTypeField] = None, + enterprise: Optional[CollaborationAllowlistExemptTargetEnterpriseField] = None, + user: Optional[UserMini] = None, + created_at: Optional[str] = None, + modified_at: Optional[str] = None, + **kwargs + ): + """ + :param id: The unique identifier for this exemption + :type id: Optional[str], optional + :param type: `collaboration_whitelist_exempt_target` + :type type: Optional[CollaborationAllowlistExemptTargetTypeField], optional + :param created_at: The time the entry was created + :type created_at: Optional[str], optional + :param modified_at: The time the entry was modified + :type modified_at: Optional[str], optional + """ + super().__init__(**kwargs) + self.id = id + self.type = type + self.enterprise = enterprise + self.user = user + self.created_at = created_at + self.modified_at = modified_at + + +class CollaborationAllowlistExemptTargets(BaseObject): + def __init__( + self, + limit: Optional[int] = None, + next_marker: Optional[int] = None, + prev_marker: Optional[int] = None, + entries: Optional[List[CollaborationAllowlistExemptTarget]] = None, + **kwargs + ): + """ + :param limit: The limit that was used for these entries. This will be the same as the + `limit` query parameter unless that value exceeded the maximum value + allowed. The maximum value varies by API. + :type limit: Optional[int], optional + :param next_marker: The marker for the start of the next page of results. + :type next_marker: Optional[int], optional + :param prev_marker: The marker for the start of the previous page of results. + :type prev_marker: Optional[int], optional + :param entries: A list of users exempt from any of the restrictions + imposed by the list of allowed collaboration domains + for this enterprise. + :type entries: Optional[List[CollaborationAllowlistExemptTarget]], optional + """ + super().__init__(**kwargs) + self.limit = limit + self.next_marker = next_marker + self.prev_marker = prev_marker + self.entries = entries + + class ShieldInformationBarrierSegmentRestriction( ShieldInformationBarrierSegmentRestrictionMini ): @@ -11184,6 +11158,7 @@ class TemplateSignerInputTypeField(str, Enum): DATE = 'date' TEXT = 'text' CHECKBOX = 'checkbox' + ATTACHMENT = 'attachment' RADIO = 'radio' DROPDOWN = 'dropdown' @@ -11246,6 +11221,7 @@ def __init__( group_id: Optional[str] = None, coordinates: Optional[TemplateSignerInputCoordinatesField] = None, dimensions: Optional[TemplateSignerInputDimensionsField] = None, + label: Optional[str] = None, document_tag_id: Optional[str] = None, text_value: Optional[str] = None, checkbox_value: Optional[bool] = None, @@ -11271,6 +11247,8 @@ def __init__( :type coordinates: Optional[TemplateSignerInputCoordinatesField], optional :param dimensions: The size of the input. :type dimensions: Optional[TemplateSignerInputDimensionsField], optional + :param label: The label field is used especially for text, attachment, radio, and checkbox type inputs. + :type label: Optional[str], optional :param document_tag_id: This references the ID of a specific tag contained in a file of the sign request. :type document_tag_id: Optional[str], optional :param text_value: Text prefill value @@ -11296,6 +11274,7 @@ def __init__( self.group_id = group_id self.coordinates = coordinates self.dimensions = dimensions + self.label = label class TemplateSignerRoleField(str, Enum): @@ -11338,6 +11317,10 @@ def __init__( self.order = order +class SignTemplateTypeField(str, Enum): + SIGN_TEMPLATE = 'sign-template' + + class SignTemplateAdditionalInfoFieldNonEditableField(str, Enum): EMAIL_SUBJECT = 'email_subject' EMAIL_MESSAGE = 'email_message' @@ -11455,6 +11438,7 @@ def __init__( class SignTemplate(BaseObject): def __init__( self, + type: Optional[SignTemplateTypeField] = None, id: Optional[str] = None, name: Optional[str] = None, email_subject: Optional[str] = None, @@ -11474,6 +11458,8 @@ def __init__( **kwargs ): """ + :param type: object type + :type type: Optional[SignTemplateTypeField], optional :param id: Template identifier. :type id: Optional[str], optional :param name: The name of the template. @@ -11507,6 +11493,7 @@ def __init__( :type custom_branding: Optional[SignTemplateCustomBrandingField], optional """ super().__init__(**kwargs) + self.type = type self.id = id self.name = name self.email_subject = email_subject @@ -11650,6 +11637,7 @@ def __init__( class UserFull(User): def __init__( self, + id: str, type: UserBaseTypeField, role: Optional[UserFullRoleField] = None, tracking_codes: Optional[List[TrackingCode]] = None, @@ -11678,10 +11666,11 @@ def __init__( notification_email: Optional[UserNotificationEmailField] = None, name: Optional[str] = None, login: Optional[str] = None, - id: Optional[str] = None, **kwargs ): """ + :param id: The unique identifier for this user + :type id: str :param type: `user` :type type: UserBaseTypeField :param role: The user’s enterprise role @@ -11748,10 +11737,9 @@ def __init__( :type name: Optional[str], optional :param login: The primary email address of this user :type login: Optional[str], optional - :param id: The unique identifier for this user - :type id: Optional[str], optional """ super().__init__( + id=id, type=type, created_at=created_at, modified_at=modified_at, @@ -11768,7 +11756,6 @@ def __init__( notification_email=notification_email, name=name, login=login, - id=id, **kwargs ) self.role = role diff --git a/box_sdk_gen/token_storage.py b/box_sdk_gen/token_storage.py new file mode 100644 index 0000000..ffffd0d --- /dev/null +++ b/box_sdk_gen/token_storage.py @@ -0,0 +1,74 @@ +import shelve +from abc import abstractmethod +from typing import Optional + +from .schemas import AccessToken + + +class TokenStorage: + @abstractmethod + def store(self, token: AccessToken) -> None: + pass + + @abstractmethod + def get(self) -> Optional[AccessToken]: + pass + + @abstractmethod + def clear(self) -> None: + pass + + +class InMemoryTokenStorage(TokenStorage): + def __init__(self): + self.token: Optional[AccessToken] = None + + def store(self, token: AccessToken) -> None: + self.token = token + + def get(self) -> Optional[AccessToken]: + return self.token + + def clear(self) -> None: + self.token = None + + +class FileTokenStorage(TokenStorage): + def __init__(self, filename: str = 'token_storage'): + self.filename = filename + + def store(self, token: AccessToken) -> None: + with shelve.open(self.filename) as file: + file['token'] = token + + def get(self) -> Optional[AccessToken]: + with shelve.open(self.filename) as file: + return file.get('token', None) + + def clear(self) -> None: + with shelve.open(self.filename) as file: + if 'token' in file: + del file['token'] + + +class FileWithInMemoryCacheTokenStorage(TokenStorage): + def __init__(self, filename: str = 'token_storage'): + self.filename = filename + self.cached_token: Optional[AccessToken] = None + + def store(self, token: AccessToken) -> None: + with shelve.open(self.filename) as file: + file['token'] = token + self.cached_token = token + + def get(self) -> Optional[AccessToken]: + if self.cached_token is None: + with shelve.open(self.filename) as file: + self.cached_token = file.get('token', None) + return self.cached_token + + def clear(self) -> None: + with shelve.open(self.filename) as file: + if 'token' in file: + del file['token'] + self.cached_token = None diff --git a/box_sdk_gen/utils.py b/box_sdk_gen/utils.py index a78f080..407ee07 100644 --- a/box_sdk_gen/utils.py +++ b/box_sdk_gen/utils.py @@ -1,8 +1,10 @@ import base64 +import hashlib +from enum import Enum from io import BytesIO, SEEK_SET, SEEK_END, BufferedIOBase import os import uuid -from typing import Dict, Optional +from typing import Dict, Optional, Iterable, Callable, TypeVar ByteStream = BufferedIOBase Buffer = bytes @@ -75,6 +77,10 @@ def buffer_equals(buffer1: Buffer, buffer2: Buffer) -> bool: return buffer1 == buffer2 +def buffer_length(buffer: Buffer) -> int: + return len(buffer) + + def decode_base_64_byte_stream(value: str) -> ByteStream: return BytesIO(base64.b64decode(value)) @@ -91,3 +97,65 @@ def to_string(value: any) -> Optional[str]: if value is None: return value return str(value) + + +class HashName(str, Enum): + SHA1 = 'sha1' + + +class Hash: + def __init__(self, algorithm: HashName): + self.algorithm = algorithm + self.hash = hashlib.sha1() + + def update_hash(self, data: Buffer): + self.hash.update(data) + + def digest_hash(self, encoding): + return base64.b64encode(self.hash.digest()).decode("utf-8") + + +def hex_to_base_64(data: hex): + return base64.b64encode(bytes.fromhex(data)).decode() + + +def list_concat(a: list, b: list): + return [*a, *b] + + +T = TypeVar('T') +Iterator = Iterable[T] +Accumulator = TypeVar('Accumulator') + + +def iterate_chunks(stream: ByteStream, chunk_size: int) -> Iterable[ByteStream]: + stream_is_finished = False + while not stream_is_finished: + copied_length = 0 + chunk = b'' + while copied_length < chunk_size: + bytes_read = stream.read(chunk_size - copied_length) + if bytes_read is None: + # stream returns none when no bytes are ready currently but there are + # potentially more bytes in the stream to be read. + continue + if not bytes_read: + # stream is exhausted. + stream_is_finished = True + break + chunk += bytes_read + copied_length += len(bytes_read) + yield BytesIO(chunk) + + +def reduce_iterator( + iterator: Iterator, + reducer: Callable[[Accumulator, T], Accumulator], + initial_value: Accumulator, +) -> Accumulator: + result = initial_value + + for item in iterator: + result = reducer(result, item) + + return result diff --git a/docs/authentication.md b/docs/authentication.md index 59af902..9fa00c4 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -311,3 +311,64 @@ def callback(): if __name__ == '__main__': app.run(port=4999) ``` + +# Token storage + +## In-memory token storage + +By default, the SDK stores the access token in volatile memory. When rerunning your application, +the access token won't be reused from the previous run; a new token has to be obtained again. +To use in-memory token storage, you don't need to do anything more than +create an Auth class using AuthConfig, for example, for OAuth: + +```python +from box_sdk_gen.oauth import OAuth, OAuthConfig + +auth = OAuth( + OAuthConfig(client_id='YOUR_CLIENT_ID', client_secret='YOUR_CLIENT_SECRET') +) +``` + +## File token storage + +If you want to keep an up-to-date access token in a file, allowing it to be reused after rerunning your application, +you can use the `FileTokenStorage` class. To enable storing the token in a file, you need to pass an object of type +`FileTokenStorage` to the AuthConfig class. For example, for OAuth: + +```python +from box_sdk_gen.oauth import OAuth, OAuthConfig +from box_sdk_gen.token_storage import FileTokenStorage + +auth = OAuth( + OAuthConfig(client_id='YOUR_CLIENT_ID', client_secret='YOUR_CLIENT_SECRET', token_storage=FileTokenStorage()) +) +``` + +## File with in-memory token storage + +If you want to keep an up-to-date access token in a file and also maintain a valid access token in in-memory cache, +allowing you to reuse the token after rerunning your application while maintaining fast access times to the token, +you can use the `FileWithInMemoryCacheTokenStorage` class. To enable storing the token in a file, +you need to pass an object of type `FileWithInMemoryCacheTokenStorage` to the AuthConfig class. For example, for OAuth: + +```python +from box_sdk_gen.oauth import OAuth, OAuthConfig +from box_sdk_gen.token_storage import FileWithInMemoryCacheTokenStorage + +auth = OAuth( + OAuthConfig(client_id='YOUR_CLIENT_ID', client_secret='YOUR_CLIENT_SECRET', token_storage=FileWithInMemoryCacheTokenStorage()) +) +``` + +## Custom storage + +You can also provide a custom token storage class. All you need to do is create a class that inherits from `TokenStorage` +and implements all of its abstract methods. Then, pass an instance of your class to the AuthConfig constructor. + +```python +from box_sdk_gen.oauth import OAuth, OAuthConfig + +auth = OAuth( + OAuthConfig(client_id='YOUR_CLIENT_ID', client_secret='YOUR_CLIENT_SECRET', token_storage=MyCustomTokenStorage()) +) +``` diff --git a/docs/chunked_uploads.md b/docs/chunked_uploads.md index f43f6dd..f693864 100644 --- a/docs/chunked_uploads.md +++ b/docs/chunked_uploads.md @@ -9,6 +9,8 @@ This is a manager for chunked uploads (allowed for files at least 20MB). - [Remove upload session](#remove-upload-session) - [List parts](#list-parts) - [Commit upload session](#commit-upload-session) +- [](#) +- [](#) ## Create upload session @@ -212,3 +214,43 @@ Returns the file object in a list.Returns when all chunks have been uploaded but Inspect the upload session to get more information about the progress of processing the chunks, then retry committing the file when all chunks have processed. + +## + +This operation is performed by calling function `reducer`. + +See the endpoint docs at +[API Reference](https://developer.box.com/reference//). + +_Currently we don't have an example for calling `reducer` in integration tests_ + +### Arguments + +- acc `PartAccumulator` + - +- chunk `ByteStream` + - + +## + +This operation is performed by calling function `upload_big_file`. + +See the endpoint docs at +[API Reference](https://developer.box.com/reference//). + + + +```python +client.chunked_uploads.upload_big_file(file_byte_stream, file_name, file_size, parent_folder_id) +``` + +### Arguments + +- file `ByteStream` + - The stream of the file to upload. +- file_name `str` + - The name of the file, which will be used for storage in Box. +- file_size `int` + - The total size of the file for the chunked upload in bytes. +- parent_folder_id `str` + - The ID of the folder where the file should be uploaded. diff --git a/docs/collaboration_allowlist_entries.md b/docs/collaboration_allowlist_entries.md index f8b03a7..ebc3b27 100644 --- a/docs/collaboration_allowlist_entries.md +++ b/docs/collaboration_allowlist_entries.md @@ -15,7 +15,11 @@ This operation is performed by calling function `get_collaboration_whitelist_ent See the endpoint docs at [API Reference](https://developer.box.com/reference/get-collaboration-whitelist-entries/). -_Currently we don't have an example for calling `get_collaboration_whitelist_entries` in integration tests_ + + +```python +client.collaboration_allowlist_entries.get_collaboration_whitelist_entries() +``` ### Arguments @@ -42,7 +46,11 @@ This operation is performed by calling function `create_collaboration_whitelist_ See the endpoint docs at [API Reference](https://developer.box.com/reference/post-collaboration-whitelist-entries/). -_Currently we don't have an example for calling `create_collaboration_whitelist_entry` in integration tests_ + + +```python +client.collaboration_allowlist_entries.create_collaboration_whitelist_entry(domain=domain, direction=direction) +``` ### Arguments @@ -69,7 +77,11 @@ This operation is performed by calling function `get_collaboration_whitelist_ent See the endpoint docs at [API Reference](https://developer.box.com/reference/get-collaboration-whitelist-entries-id/). -_Currently we don't have an example for calling `get_collaboration_whitelist_entry_by_id` in integration tests_ + + +```python +client.collaboration_allowlist_entries.get_collaboration_whitelist_entry_by_id(collaboration_whitelist_entry_id=entry.id) +``` ### Arguments @@ -94,7 +106,11 @@ This operation is performed by calling function `delete_collaboration_whitelist_ See the endpoint docs at [API Reference](https://developer.box.com/reference/delete-collaboration-whitelist-entries-id/). -_Currently we don't have an example for calling `delete_collaboration_whitelist_entry_by_id` in integration tests_ + + +```python +client.collaboration_allowlist_entries.delete_collaboration_whitelist_entry_by_id(collaboration_whitelist_entry_id=entry.id) +``` ### Arguments diff --git a/docs/collaboration_allowlist_exempt_targets.md b/docs/collaboration_allowlist_exempt_targets.md index a5cfdf0..9f83c69 100644 --- a/docs/collaboration_allowlist_exempt_targets.md +++ b/docs/collaboration_allowlist_exempt_targets.md @@ -15,7 +15,11 @@ This operation is performed by calling function `get_collaboration_whitelist_exe See the endpoint docs at [API Reference](https://developer.box.com/reference/get-collaboration-whitelist-exempt-targets/). -_Currently we don't have an example for calling `get_collaboration_whitelist_exempt_targets` in integration tests_ + + +```python +client.collaboration_allowlist_exempt_targets.get_collaboration_whitelist_exempt_targets() +``` ### Arguments @@ -42,7 +46,11 @@ This operation is performed by calling function `create_collaboration_whitelist_ See the endpoint docs at [API Reference](https://developer.box.com/reference/post-collaboration-whitelist-exempt-targets/). -_Currently we don't have an example for calling `create_collaboration_whitelist_exempt_target` in integration tests_ + + +```python +client.collaboration_allowlist_exempt_targets.create_collaboration_whitelist_exempt_target(user=CreateCollaborationWhitelistExemptTargetUserArg(id=user.id)) +``` ### Arguments @@ -67,7 +75,11 @@ This operation is performed by calling function `get_collaboration_whitelist_exe See the endpoint docs at [API Reference](https://developer.box.com/reference/get-collaboration-whitelist-exempt-targets-id/). -_Currently we don't have an example for calling `get_collaboration_whitelist_exempt_target_by_id` in integration tests_ + + +```python +client.collaboration_allowlist_exempt_targets.get_collaboration_whitelist_exempt_target_by_id(collaboration_whitelist_exempt_target_id=exempt_target.id) +``` ### Arguments @@ -92,7 +104,11 @@ This operation is performed by calling function `delete_collaboration_whitelist_ See the endpoint docs at [API Reference](https://developer.box.com/reference/delete-collaboration-whitelist-exempt-targets-id/). -_Currently we don't have an example for calling `delete_collaboration_whitelist_exempt_target_by_id` in integration tests_ + + +```python +client.collaboration_allowlist_exempt_targets.delete_collaboration_whitelist_exempt_target_by_id(collaboration_whitelist_exempt_target_id=exempt_target.id) +``` ### Arguments diff --git a/docs/folder_metadata.md b/docs/folder_metadata.md index fde7004..32abbb1 100644 --- a/docs/folder_metadata.md +++ b/docs/folder_metadata.md @@ -59,7 +59,7 @@ _Currently we don't have an example for calling `get_folder_metadata_by_id` in i ### Returns -This function returns a value of type `Metadata`. +This function returns a value of type `MetadataFull`. An instance of the metadata template that includes additional "key:value" pairs defined by the user or diff --git a/migration-guide.md b/migration-guide.md index 0e75ac5..3649eab 100644 --- a/migration-guide.md +++ b/migration-guide.md @@ -21,6 +21,7 @@ - [OAuth 2.0 Auth](#oauth-20-auth) - [Get Authorization URL](#get-authorization-url) - [Authenticate](#authenticate) + - [Store token and retrieve token callbacks](#store-token-and-retrieve-token-callbacks) @@ -410,3 +411,87 @@ from box_sdk_gen.client import Client access_token = auth.get_tokens_authorization_code_grant('YOUR_AUTH_CODE') client = Client(auth) ``` + +### Store token and retrieve token callbacks + +In old SDK you could provide a `store_tokens` callback method to an authentication class, which was called each time +an access token was refreshed. It could be used to save your access token to a custom token storage +and allow to reuse this token later. +What is more, old SDK allowed also to provide `retrieve_tokens` callback, which is called each time the SDK needs to use +token to perform an API call. To provide that, it was required to use `CooperativelyManagedOAuth2` and provide +`retrieve_tokens` callback method to its constructor. + +**Old (`boxsdk`)** + +```python +from typing import Tuple +from boxsdk.auth import CooperativelyManagedOAuth2 + +def retrieve_tokens() -> Tuple[str, str]: + # retrieve access_token and refresh_token + return access_token, refresh_token + +def store_tokens(access_token: str, refresh_token: str): + # store access_token and refresh_token + pass + + +auth = CooperativelyManagedOAuth2( + client_id='YOUR_CLIENT_ID', + client_secret='YOUR_CLIENT_SECRET', + retrieve_tokens=retrieve_tokens, + store_tokens=store_tokens +) +access_token, refresh_token = auth.authenticate('YOUR_AUTH_CODE') +client = Client(auth) +``` + +In the new SDK you can define your own class delegated for storing and retrieving a token. It has to inherit from +`TokenStorage` and implement all of its abstract methods. Then, pass an instance of this class to the +AuthConfig constructor. + +**New (`box-sdk-gen`)** + +```python +from typing import Optional +from box_sdk_gen.oauth import OAuth, OAuthConfig +from box_sdk_gen.token_storage import FileWithInMemoryCacheTokenStorage, TokenStorage +from .schemas import AccessToken + +class MyCustomTokenStorage(TokenStorage): + def store(self, token: AccessToken) -> None: + # store token + pass + + def get(self) -> Optional[AccessToken]: + # get token + pass + + def clear(self) -> None: + # clear token + pass + + +auth = OAuth( + OAuthConfig( + client_id='YOUR_CLIENT_ID', + client_secret='YOUR_CLIENT_SECRET', + token_storage=MyCustomTokenStorage() + ) +) +``` + +or reuse one of the provided implementations: `FileTokenStorage` or `FileWithInMemoryCacheTokenStorage`: + +```python +from box_sdk_gen.oauth import OAuth, OAuthConfig +from box_sdk_gen.token_storage import FileWithInMemoryCacheTokenStorage + +auth = OAuth( + OAuthConfig( + client_id='YOUR_CLIENT_ID', + client_secret='YOUR_CLIENT_SECRET', + token_storage=FileWithInMemoryCacheTokenStorage() + ) +) +``` diff --git a/test/chunked_uploads.py b/test/chunked_uploads.py new file mode 100644 index 0000000..2562d68 --- /dev/null +++ b/test/chunked_uploads.py @@ -0,0 +1,39 @@ +from box_sdk_gen.utils import ByteStream + +from box_sdk_gen.client import Client + +from box_sdk_gen.jwt_auth import JWTAuth + +from box_sdk_gen.jwt_auth import JWTConfig + +from box_sdk_gen.utils import decode_base_64 + +from box_sdk_gen.utils import get_env_var + +from box_sdk_gen.utils import get_uuid + +from box_sdk_gen.utils import generate_byte_stream + +from box_sdk_gen.schemas import File + +jwt_config: JWTConfig = JWTConfig.from_config_json_string( + decode_base_64(get_env_var('JWT_CONFIG_BASE_64')) +) + +auth: JWTAuth = JWTAuth(config=jwt_config) + +client: Client = Client(auth=auth) + + +def testChunkedUpload(): + file_size: int = (20 * 1024) * 1024 + file_byte_stream: ByteStream = generate_byte_stream(file_size) + file_name: str = get_uuid() + parent_folder_id: str = '0' + uploaded_file: File = client.chunked_uploads.upload_big_file( + file_byte_stream, file_name, file_size, parent_folder_id + ) + assert uploaded_file.name == file_name + assert uploaded_file.size == file_size + assert uploaded_file.parent.id == parent_folder_id + client.files.delete_file_by_id(file_id=uploaded_file.id) diff --git a/test/collaboration_allowlist_entries.py b/test/collaboration_allowlist_entries.py new file mode 100644 index 0000000..43bd075 --- /dev/null +++ b/test/collaboration_allowlist_entries.py @@ -0,0 +1,57 @@ +import pytest + +from box_sdk_gen.schemas import CollaborationAllowlistEntries + +from box_sdk_gen.schemas import CollaborationAllowlistEntry + +from box_sdk_gen.utils import decode_base_64 + +from box_sdk_gen.utils import get_env_var + +from box_sdk_gen.utils import get_uuid + +from box_sdk_gen.client import Client + +from box_sdk_gen.jwt_auth import JWTAuth + +from box_sdk_gen.jwt_auth import JWTConfig + +client: Client = Client( + auth=JWTAuth( + config=JWTConfig.from_config_json_string( + decode_base_64(get_env_var('JWT_CONFIG_BASE_64')) + ) + ) +) + + +def collaborationAllowlistEntries(): + allowlist: CollaborationAllowlistEntries = ( + client.collaboration_allowlist_entries.get_collaboration_whitelist_entries() + ) + assert len(allowlist.entries) >= 0 + direction: str = 'inbound' + domain: str = 'example.com' + new_entry: CollaborationAllowlistEntry = ( + client.collaboration_allowlist_entries.create_collaboration_whitelist_entry( + domain=domain, direction=direction + ) + ) + assert new_entry.type == 'collaboration_whitelist_entry' + assert new_entry.direction == direction + assert new_entry.domain == domain + entry: CollaborationAllowlistEntry = ( + client.collaboration_allowlist_entries.get_collaboration_whitelist_entry_by_id( + collaboration_whitelist_entry_id=new_entry.id + ) + ) + assert entry.id == new_entry.id + assert entry.direction == direction + assert entry.domain == domain + client.collaboration_allowlist_entries.delete_collaboration_whitelist_entry_by_id( + collaboration_whitelist_entry_id=entry.id + ) + with pytest.raises(Exception): + client.collaboration_allowlist_entries.get_collaboration_whitelist_entry_by_id( + collaboration_whitelist_entry_id=entry.id + ) diff --git a/test/collaboration_allowlist_exempt_targets.py b/test/collaboration_allowlist_exempt_targets.py new file mode 100644 index 0000000..8858a57 --- /dev/null +++ b/test/collaboration_allowlist_exempt_targets.py @@ -0,0 +1,61 @@ +import pytest + +from box_sdk_gen.schemas import CollaborationAllowlistExemptTargets + +from box_sdk_gen.schemas import User + +from box_sdk_gen.schemas import CollaborationAllowlistExemptTarget + +from box_sdk_gen.managers.collaboration_allowlist_exempt_targets import ( + CreateCollaborationWhitelistExemptTargetUserArg, +) + +from box_sdk_gen.utils import decode_base_64 + +from box_sdk_gen.utils import get_env_var + +from box_sdk_gen.utils import get_uuid + +from box_sdk_gen.client import Client + +from box_sdk_gen.jwt_auth import JWTAuth + +from box_sdk_gen.jwt_auth import JWTConfig + +client: Client = Client( + auth=JWTAuth( + config=JWTConfig.from_config_json_string( + decode_base_64(get_env_var('JWT_CONFIG_BASE_64')) + ) + ) +) + + +def collaborationAllowlistExemptTargets(): + exempt_targets: CollaborationAllowlistExemptTargets = ( + client.collaboration_allowlist_exempt_targets.get_collaboration_whitelist_exempt_targets() + ) + assert len(exempt_targets.entries) >= 0 + user: User = client.users.create_user( + name=get_uuid(), + login=''.join([get_uuid(), '@boxdemo.com']), + is_platform_access_only=True, + ) + new_exempt_target: CollaborationAllowlistExemptTarget = client.collaboration_allowlist_exempt_targets.create_collaboration_whitelist_exempt_target( + user=CreateCollaborationWhitelistExemptTargetUserArg(id=user.id) + ) + assert new_exempt_target.type == 'collaboration_whitelist_exempt_target' + assert new_exempt_target.user.id == user.id + exempt_target: CollaborationAllowlistExemptTarget = client.collaboration_allowlist_exempt_targets.get_collaboration_whitelist_exempt_target_by_id( + collaboration_whitelist_exempt_target_id=new_exempt_target.id + ) + assert exempt_target.id == new_exempt_target.id + assert exempt_target.user.id == user.id + client.collaboration_allowlist_exempt_targets.delete_collaboration_whitelist_exempt_target_by_id( + collaboration_whitelist_exempt_target_id=exempt_target.id + ) + with pytest.raises(Exception): + client.collaboration_allowlist_exempt_targets.get_collaboration_whitelist_exempt_target_by_id( + collaboration_whitelist_exempt_target_id=exempt_target.id + ) + client.users.delete_user_by_id(user_id=user.id) diff --git a/test/commons.py b/test/commons.py index 16fafe2..483b84f 100644 --- a/test/commons.py +++ b/test/commons.py @@ -37,7 +37,7 @@ def upload_new_file() -> File: new_file_name: str = ''.join([get_uuid(), '.pdf']) - file_content_stream: ByteStream = generate_byte_stream(1048576) + file_content_stream: ByteStream = generate_byte_stream(1024 * 1024) uploaded_files: Files = client.uploads.upload_file( attributes=UploadFileAttributesArg( name=new_file_name, parent=UploadFileAttributesArgParentField(id='0') diff --git a/test/recent_items.py b/test/recent_items.py index 2593e98..052b6fb 100644 --- a/test/recent_items.py +++ b/test/recent_items.py @@ -23,4 +23,4 @@ def testRecentItems(): auth.as_user(get_env_var('USER_ID')) client: Client = Client(auth=auth) recent_items: RecentItems = client.recent_items.get_recent_items() - assert len(recent_items.entries) > 0 + assert len(recent_items.entries) >= 0 diff --git a/test/trashed_files.py b/test/trashed_files.py index f07098f..b5ba4f9 100644 --- a/test/trashed_files.py +++ b/test/trashed_files.py @@ -40,7 +40,7 @@ def testTrashedFiles(): - file_size = 1024 * 1024 + file_size: int = 1024 * 1024 file_name: str = get_uuid() file_byte_stream: ByteStream = generate_byte_stream(file_size) files: Files = client.uploads.upload_file(