Skip to content

Latest commit

 

History

History
774 lines (567 loc) · 24.9 KB

File metadata and controls

774 lines (567 loc) · 24.9 KB

Migration guide: migrate from boxsdk to box_sdk_gen package

Introduction

Version availability:

  • v3: ships only boxsdk package
  • v4: ships both boxsdk and box_sdk_gen packages (side-by-side)
  • v10+: ships only box_sdk_gen module

This document focuses on helping you migrate code from the manually maintained boxsdk module to the generated box_sdk_gen package. Many APIs were redesigned for consistency and modern Python patterns, so this guide calls out how to adopt the new shapes safely and incrementally.

Supported migration paths:

  • v3 → v4: adopt box_sdk_gen gradually while keeping existing boxsdk usage
  • v3 → v10+: migrate directly to box_sdk_gen package only
  • v4 (within the same version): move usage from boxsdk to box_sdk_gen gradually

For comprehensive API docs with sample code for all methods, see the repository documentation in the root docs directory: docs/.

We recommend using box_sdk_gen as the preferred SDK going forward. This SDK is automatically generated from the Box OpenAPI specification, ensuring consistency, reliability, and full API coverage. Key Benefits:

  • Comprehensive Coverage: Supports all Box API endpoints with consistent and predictable method signatures.
  • Rapid Feature Availability: Automatically includes new features as soon as they are released in the Box API.
  • Strong Typing: Provides complete type hints for every method and data structure, improving development efficiency and reducing runtime errors.
  • Explicit Data Models: Includes clear, well-defined models for all API resources to improve readability and maintainability.
  • Immutable Design: Built for immutability, making code behavior more predictable and easier to reason about.
  • Rich Documentation: Offers detailed usage examples for every API method to help developers get started quickly.

Who is this for?

  • Developers with existing code using boxsdk who want to start using box_sdk_gen APIs.
  • Developers using v4 of Box Python SDK that want to transition usage from boxsdk to box_sdk_gen within the same app.

Key differences

Manager approach

The main difference between the manual boxsdk and generated box_sdk_gen package is the way how API methods are aggregated into objects.

Old (boxsdk)

Firstly, in the boxsdk package, to be able to perform any action on an API object, e.g. User, you first had to create its class. To do it is required to call:

user = client.user(user_id="123456")

to create a class representing an already existing User with id '12345', or create a new one with a call:

user = client.create_user(name="Some User")

Then, you could perform any action on created class, which will affect the user, e.g.

updated_user = user.update_info(data={"name": "New User Name"})

New (box_sdk_gen)

In the box_sdk_gen package, the API methods are grouped into dedicated manager classes, e.g. User object has dedicated UserManager class. Each manager class instance is available in BoxClient object. The fields storing references to the managers are named in the plural form of the resource that the manager handles - client.users for UsersManager. If you want to perform any operation connected with a User you need to call a respective method of UserManager. For example, to get info about existing user you need to call:

user = client.users.get_user_by_id(user_id="123456")

or to create a new user:

user = client.users.create_user(name="Some User")

The User object returned by both of these methods is a data class - it does not contain any methods to call. To perform any action on User object, you need to still use a UserManager method for that. Usually these methods have a first argument, which accepts id of the object you want to access, e.g. to update a user's name, call method:

updated_user = client.users.update_user_by_id(user_id=user.id, name="New User Name")

Explicitly defined schemas

Old (boxsdk)

In boxsdk package, there were no data types explicitly defined - the responses were dynamically mapped into classes in the runtime. For example, if you get information about a file:

file = client.file(file_id="12345678").get()

you couldn't be sure which fields to expect in the response object until the runtime, because File class doesn't have any predefined fields.

New (box_sdk_gen)

In box_sdk_gen package, the data classes are defined in schemas module, so you know, which fields to expect before actually making a call. For example FileBase class is defined this way:

class FileBase(BaseObject):
    def __init__(
        self,
        id: str,
        *,
        etag: Optional[str] = None,
        type: FileBaseTypeField = FileBaseTypeField.FILE.value,
        **kwargs
    ):
        super().__init__(**kwargs)
        self.id = id
        self.type = type
        self.etag = etag

Immutable design

The new box_sdk_gen package is designed to be mostly immutable. This means that methods, which used to modify the existing object in boxsdk package now return a new instance of the class with the modified state. This design pattern is used to avoid side effects and make the code more predictable and easier to reason about. Methods, which returns a new modified instance of an object, will always have a prefix with_ in their names, e.g.

New (box_sdk_gen)

from box_sdk_gen import BoxClient

as_user_client: BoxClient = client.with_as_user_header("USER_ID")

Authentication

The box_sdk_gen package offers the same authentication methods as boxsdk package. Let's see the differences of their usage:

Developer Token

Old (boxsdk)

from boxsdk import Client, OAuth2

auth = OAuth2(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    access_token="DEVELOPER_TOKEN_GOES_HERE",
)
client = Client(auth)

The box_sdk_gen package, provides a convenient BoxDeveloperTokenAuth, which allows authenticating using developer token without necessity to provide a Client ID and Client Secret

New (box_sdk_gen)

from box_sdk_gen import BoxClient, BoxDeveloperTokenAuth

auth = BoxDeveloperTokenAuth(token="DEVELOPER_TOKEN_GOES_HERE")
client = BoxClient(auth=auth)

JWT Auth

Using JWT configuration file

Old (boxsdk)

The static method, which reads the JWT configuration file has been changed:

from boxsdk import JWTAuth, Client

auth = JWTAuth.from_settings_file("/path/to/config.json")
client = Client(auth)

New (box_sdk_gen)

from box_sdk_gen import BoxClient, BoxJWTAuth, JWTConfig

jwt_config = JWTConfig.from_config_file(config_file_path="/path/to/config.json")
auth = BoxJWTAuth(config=jwt_config)
client = BoxClient(auth=auth)

Providing JWT configuration manually

Some params in JWTConfig constructor have slightly different names than one in the old JWTAuth class.

Old (boxsdk)

from boxsdk import JWTAuth

auth = JWTAuth(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    enterprise_id="YOUR_ENTERPRISE_ID",
    user_id="USER_ID",
    jwt_key_id="YOUR_JWT_KEY_ID",
    rsa_private_key_file_sys_path="CERT.PEM",
    rsa_private_key_passphrase="PASSPHRASE",
    jwt_algorithm="RS256",
)

New (box_sdk_gen)

from box_sdk_gen import BoxJWTAuth, JWTConfig, JwtAlgorithm

jwt_config = JWTConfig(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    enterprise_id="YOUR_ENTERPRISE_ID",
    user_id="USER_ID",
    jwt_key_id="YOUR_JWT_KEY_ID",
    private_key="YOUR_PRIVATE_KEY",
    private_key_passphrase="PASSPHRASE",
    algorithm=JwtAlgorithm.RS256,
)
auth = BoxJWTAuth(config=jwt_config)

Authenticate user

In boxsdk package, method for user authentication was named authenticate_user(self, user: Union[str, 'User'] = None) -> str and was accepting either user object or user id. If none provided, user ID stored in JWTAuth class instance was used. The authenticate_user method was modifying existing BoxJWTAuth class, which was exchanging the existing token with the one with the user access.

Old (boxsdk)

auth.authenticate_user(user)

or

auth.authenticate_user("USER_ID")

New (box_sdk_gen)

In new box_sdk_gen package, to authenticate as user you need to call with_user_subject(self, user_id: str, *, token_storage: TokenStorage = None) -> BoxJWTAuth method with id of the user to authenticate. The method returns a new instance of BoxJWTAuth class, which will perform authentication call in scope of the user on the first API call. The token_storage parameter is optional and allows to provide a custom token storage for the new instance of BoxJWTAuth class. The new auth instance can be used to create a new user client instance.

from box_sdk_gen import BoxJWTAuth, BoxClient

user_auth: BoxJWTAuth = auth.with_user_subject("USER_ID")
user_client: BoxClient = BoxClient(auth=user_auth)

Client Credentials Grant

Obtaining Service Account token

To authenticate as enterprise, the only difference between the versions of SDK, is using the CCGConfig as a middle step.

Old (boxsdk)

from boxsdk import CCGAuth, Client

auth = CCGAuth(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    enterprise_id="YOUR_ENTERPRISE_ID",
)

client = Client(auth)

New (box_sdk_gen)

from box_sdk_gen import BoxClient, BoxCCGAuth, CCGConfig

ccg_config = CCGConfig(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    enterprise_id="YOUR_ENTERPRISE_ID",
)
auth = BoxCCGAuth(config=ccg_config)
client = BoxClient(auth=auth)

Obtaining User token

In boxsdk package CCGAuth was accepting both user object and User ID. In box_sdk_gen package the BoxCCGAuth constructor accepts only User ID instead.

Old (boxsdk)

from boxsdk import CCGAuth

auth = CCGAuth(
    client_id="YOUR_CLIENT_ID", client_secret="YOUR_CLIENT_SECRET", user="YOUR_USER_ID"
)

New (box_sdk_gen)

from box_sdk_gen import BoxCCGAuth, CCGConfig

ccg_config = CCGConfig(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    user_id="YOUR_USER_ID",
)
auth = BoxCCGAuth(config=ccg_config)

Switching between Service Account and User

In boxsdk package, there were two methods which allowed to switch between using service and user account. Calling these methods were modifying existing state of CCGAuth class, which was fetching a new token on the next API call.

Old (boxsdk)

auth.authenticate_enterprise("ENTERPRISE_ID")
auth.authenticate_user("USER_ID")

In the box_sdk_gen package, to keep the immutability design, the methods switching authenticated subject were replaced with methods returning a new instance of BoxCCGAuth class. The new instance will fetch a new token on the next API call. The new auth instance can be used to create a new client instance. You can also specify token_storage parameter to provide a custom token storage for the new instance. The old instance of BoxCCGAuth class will remain unchanged and will still use the old token.

New (box_sdk_gen)

from box_sdk_gen import BoxCCGAuth, BoxClient

enterprise_auth: BoxCCGAuth = auth.with_enterprise_subject(
    enterprise_id="ENTERPRISE_ID"
)
enterprise_client: BoxClient = BoxClient(auth=enterprise_auth)
from box_sdk_gen import BoxCCGAuth, BoxClient

user_auth: BoxCCGAuth = auth.with_user_subject(user_id="USER_ID")
user_client: BoxClient = BoxClient(auth=user_auth)

Note that the new methods accept only user id or enterprise id, while the old ones were accepting user and enterprise object too.

OAuth 2.0 Auth

Get Authorization URL

To get authorization url in the box_sdk_gen package, you need to first create the BoxOAuth class (previously OAuth2) using OAuthConfig class. Then to get authorization url, call get_authorize_url(self, *, options: GetAuthorizeUrlOptions = None) -> str instead of get_authorization_url(self, redirect_url: Optional[str]) -> Tuple[str, str]. Note that this method now accepts the instance of GetAuthorizeUrlOptions class, which allows specifying extra options to API call. The new function returns only the authentication url string, while the old one returns tuple of authentication url and csrf_token.

Old (boxsdk)

from boxsdk import OAuth2

auth = OAuth2(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
)

auth_url, csrf_token = auth.get_authorization_url("http://YOUR_REDIRECT_URL")

New (box_sdk_gen)

from box_sdk_gen import BoxOAuth, OAuthConfig, GetAuthorizeUrlOptions

auth = BoxOAuth(
    OAuthConfig(
        client_id="YOUR_CLIENT_ID",
        client_secret="YOUR_CLIENT_SECRET",
    )
)
auth_url = auth.get_authorize_url(
    options=GetAuthorizeUrlOptions(redirect_uri="http://YOUR_REDIRECT_URL")
)

Authenticate

The signature of method for authenticating with obtained auth code got changed from: authenticate(self, auth_code: Optional[str]) -> Tuple[str, str] to get_tokens_authorization_code_grant(self, authorization_code: str, *, network_session: Optional[NetworkSession] = None) -> AccessToken. The method now returns an AccessToken object with access_token and refresh_token fields, while the old one was returning a tuple of access token and refresh token.

Old (boxsdk)

from boxsdk import Client

access_token, refresh_token = auth.authenticate("YOUR_AUTH_CODE")
client = Client(auth)

New (box_sdk_gen)

from box_sdk_gen import BoxClient, AccessToken

access_token: AccessToken = auth.get_tokens_authorization_code_grant("YOUR_AUTH_CODE")
client = BoxClient(auth)

Store token and retrieve token callbacks

In boxsdk package 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, boxsdk package 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)

from typing import Tuple
from boxsdk.auth import CooperativelyManagedOAuth2
from boxsdk import Client


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 box_sdk_gen package, 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. Next step would be to pass an instance of this class to the AuthConfig constructor.

New (box_sdk_gen)

from typing import Optional
from box_sdk_gen import BoxOAuth, OAuthConfig, TokenStorage, 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 = BoxOAuth(
    OAuthConfig(
        client_id="YOUR_CLIENT_ID",
        client_secret="YOUR_CLIENT_SECRET",
        token_storage=MyCustomTokenStorage(),
    )
)

or reuse one of the provided implementations: FileTokenStorage or FileWithInMemoryCacheTokenStorage:

from box_sdk_gen import BoxOAuth, OAuthConfig, FileWithInMemoryCacheTokenStorage

auth = BoxOAuth(
    OAuthConfig(
        client_id="YOUR_CLIENT_ID",
        client_secret="YOUR_CLIENT_SECRET",
        token_storage=FileWithInMemoryCacheTokenStorage(),
    )
)

Downscope token

The process of downscoping token in the new package is similar to the old one. The main difference is that the new method accepts the full resource path instead of file object.

Old (boxsdk)

from boxsdk import Client, OAuth2

target_file = client.file(file_id="FILE_ID_HERE")
token_info = client.downscope_token(["item_preview"], target_file)
downscoped_auth = OAuth2(
    client_id=None, client_secret=None, access_token=token_info.access_token
)
downscoped_client = Client(downscoped_auth)

New (box_sdk_gen)

from box_sdk_gen import BoxDeveloperTokenAuth, AccessToken, BoxClient

resource = "https://api.box.com/2.0/files/123456789"
downscoped_token: AccessToken = auth.downscope_token(
    scopes=["item_preview"],
    resource=resource,
)
downscoped_auth = BoxDeveloperTokenAuth(token=downscoped_token.access_token)
client = BoxClient(auth=downscoped_auth)

Revoke token

To revoke current client's tokens in the box_sdk_gen package, you need to call revoke_token method of the auth class instead of revoke method.

Old (boxsdk)

oauth.revoke()

New (box_sdk_gen)

client.auth.revoke_token()

Configuration

As-User header

The As-User header is used by enterprise admins to make API calls on behalf of their enterprise's users. This requires the API request to pass an As-User: USER-ID header. The following examples assume that the client has been instantiated with an access token with appropriate privileges to make As-User calls.

In boxsdk package you could call client as_user(self, user: User) method to create a new client to impersonate the provided user.

Old (boxsdk)

from boxsdk import Client

user_to_impersonate = client.user(user_id="USER_ID")
user_client: Client = client.as_user(user_to_impersonate)

New (box_sdk_gen)

In box_sdk_gen package the method was renamed to with_as_user_header(self, user_id: str) -> BoxClient and returns a new instance of BoxClient class with the As-User header appended to all API calls made by the client. The method accepts only user id as a parameter.

from box_sdk_gen import BoxClient

user_client: BoxClient = client.with_as_user_header(user_id="USER_ID")

Additionally BoxClient offers a with_extra_headers(self, *, extra_headers: Dict[str, str] = None) -> BoxClient method, which allows you to specify the custom set of headers, which will be included in every API call made by client. Calling the client.with_extra_headers() method creates a new client, leaving the original client unmodified.

from box_sdk_gen import BoxClient

new_client: BoxClient = client.with_extra_headers(
    extra_headers={"customHeader": "customValue"}
)

Custom Base URLs

Old (boxsdk)

In manual boxsdk package, you could specify the custom base URLs, which will be used for API calls made by setting the new values of static variables of the API class.

from boxsdk.config import API

API.BASE_API_URL = "https://new-base-url.com"
API.OAUTH2_API_URL = "https://my-company.com/oauth2"
API.UPLOAD_URL = "https://my-company-upload-url.com"

New (box_sdk_gen)

In the new package this functionality has been implemented as part of the BoxClient class. By calling the client.with_custom_base_urls() method, you can specify the custom base URLs that will be used for API calls made by client. Following the immutability pattern, this call creates a new client, leaving the original client unmodified.

from box_sdk_gen import BoxClient, BaseUrls

new_client: BoxClient = client.with_custom_base_urls(
    base_urls=BaseUrls(
        base_url="https://new-base-url.com",
        upload_url="https://my-company-upload-url.com",
        oauth_2_url="https://my-company.com/oauth2",
    )
)

Convenience methods

Webhook validation

Webhook validation is used to validate a webhook message by verifying the signature and the delivery timestamp.

Old (boxsdk)

In the boxsdk package of Box Python SDK, you could pass the body as bytes, and it would return a boolean value indicating whether the message was valid.

body = b'{"webhook":{"id":"1234567890"},"trigger":"FILE.UPLOADED","source":{"id":"1234567890","type":"file","name":"Test.txt"}}'
headers = {
    "box-delivery-id": "f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f",
    "box-delivery-timestamp": "2020-01-01T00:00:00-07:00",
    "box-signature-algorithm": "HmacSHA256",
    "box-signature-primary": "4KvFa5/unRL8aaqOlnbInTwkOmieZkn1ZVzsAJuRipE=",
    "box-signature-secondary": "yxxwBNk7tFyQSy95/VNKAf1o+j8WMPJuo/KcFc7OS0Q=",
    "box-signature-version": "1",
}
is_validated = Webhook.validate_message(body, headers, primary_key, secondary_key)
print(f"The webhook message is validated to: {is_validated}")

New (box_sdk_gen)

In the new box_sdk_gen package, the WebhooksManager.validate_message() method requires the body to be of type string and the rest of the code remains the same

from box_sdk_gen import WebhooksManager

body = '{"webhook":{"id":"1234567890"},"trigger":"FILE.UPLOADED","source":{"id":"1234567890","type":"file","name":"Test.txt"}}'
headers = {
    "box-delivery-id": "f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f",
    "box-delivery-timestamp": "2020-01-01T00:00:00-07:00",
    "box-signature-algorithm": "HmacSHA256",
    "box-signature-primary": "4KvFa5/unRL8aaqOlnbInTwkOmieZkn1ZVzsAJuRipE=",
    "box-signature-secondary": "yxxwBNk7tFyQSy95/VNKAf1o+j8WMPJuo/KcFc7OS0Q=",
    "box-signature-version": "1",
}
WebhooksManager.validate_message(
    body=body, headers=headers, primary_key=primary_key, secondary_key=secondary_key
)

Chunked upload of big files

For large files or in cases where the network connection is less reliable, you may want to upload the file in parts. This allows a single part to fail without aborting the entire upload, and failed parts are being retried automatically.

Old (boxsdk)

In boxsdk, you could use the get_chunked_uploader() method to create a chunked uploader object. Then, you would call the start() method to begin the upload process. The get_chunked_uploader() method requires the file_path and file_name parameters.

chunked_uploader = client.folder("0").get_chunked_uploader(
    file_path="/path/to/file.txt", file_name="new_name.txt"
)
uploaded_file = chunked_uploader.start()
print(f'File "{uploaded_file.name}" uploaded to Box with file ID {uploaded_file.id}')

New (box_sdk_gen)

In box_sdk_gen, the equivalent method is chunked_uploads.upload_big_file(). It accepts a file-like object as the file parameter, and the file_name and file_size parameters are now passed as arguments. The parent_folder_id parameter is also required to specify the folder where the file will be uploaded.

import os

with open("/path/to/file.txt", "rb") as file_byte_stream:
    file_name = "new_name.txt"
    file_size = os.path.getsize("/path/to/file.txt")
    parent_folder_id = "0"  # ID of the folder where the file will be uploaded
    uploaded_file = client.chunked_uploads.upload_big_file(
        file=file_byte_stream,
        file_name=file_name,
        file_size=file_size,
        parent_folder_id=parent_folder_id,
    )