Skip to main content
← Back to the liblab blog

Introducing version 2 of our Python SDK generation

We're thrilled to announce the preview launch of our revamped Python SDKs, bringing new capabilities and improvements to simplify your development experience. We've focused on enhancing usability, efficiency, and overall developer satisfaction. From seamless input handling, to automated validation and intuitive request building, our v2 SDKs are designed to empower developers like never before.

In this article, we'll take a closer look at the key features and enhancements introduced in our v2 Python SDKs, showcasing how they can accelerate your development process and streamline interaction with APIs. All the code snippets you'll encounter have been automatically generated using liblab, using v2 of our Python SDKs, and utilizing samples of OpenAPI Specs.

Casting the input

Simple is better than complex. Complex is better than complicated.

With the release of the new version of the Python SDKs, we are introducing a new feature called cast_models. This tool is designed to simplify the process of handling user input by automatically converting values into model instances. By doing so, users can concentrate more on the core logic of the SDK and not get overwhelmed by the complexities of input handling.

To understand how it works, consider the following example:

@cast_models
def create(self, request_body: CreateRequest) -> ApiResponse:
...

Here, the cast_models decorator is applied to the create method. This means that developers can simply invoke create with a dictionary representing the request_body, and it will seamlessly transform into an instance of CreateRequest.

The cast_models feature not only facilitates the conversion of dictionaries to models but also adeptly handles the conversion of primitives into their expected types. For instance, "2" is automatically cast to 2, Enum values are transformed into Enum instances, and types within lists are cast effortlessly, among other capabilities.

Validating the input

Errors should never pass silently.

Once the input is cast into their expected types through cast_models, the next step is ensuring the integrity and validity of the data. In our v2 Python SDKs, we are employing a new validation mechanism to accomplish this task.

Let's take a closer look at how validation is integrated into our workflow with a practical example:

...
"parameters": [
{
"name": "LlamaId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"description": "The ID of a llama",
"title": "LlamaId",
"pattern": "^uuid:\\d+$"
},
},
{
"name": "limit",
"required": true,
"in": "query",
"example": 10,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100
}
},
],
...

In this OpenAPI spec, the LlamaId parameter is defined as a uuid, and the limit parameter is subject to a minimum and maximum. These constraints are automatically translated into validation rules in our Python SDKs.

@cast_models
def get_something(
self,
llama_id: str,
limit: int,
sort_by: SortBy = None,
tags: List[float] = None,
) -> PaginatedSomethingResponse:

...
Validator(str).pattern("^uuid:\d+$").validate(llama_id)
Validator(int).min(1).max(100).validate(limit)
Validator(SortBy).is_optional().validate(sort_by)
Validator(float).is_array().is_optional().validate(tags)

In the code snippet above, the validation rules are automatically generated based on the API specification. For instance, the llama_id parameter is validated against a pattern match to ensure it conforms to the format "uuid:[number]". Similarly, the limit parameter is subject to minimum and maximum constraints, with their values dictated by the API spec.

Each parameter is submitted to validation to ensure compliance with the criteria defined in the API specification, maintaining data integrity and preventing unnecessary API consumption.

Building the request

There should be one - and preferably only one - obvious way to do it.

Once the input parameters are cast and validated, the next step is to build the request. We achieve this using a simplified and intuitive approach within our Python SDKs.

Let's dive into how we build the requests using a practical example:

@cast_models
def get_something(
self,
llama_id: str,
limit: int,
sort_by: SortBy = None,
tags: List[float] = None,
) -> PaginatedSomethingResponse:
...
Validator(float).is_array().is_optional().validate(tags)

serialized_request = (
Serializer(f"{self.base_url}/something/{{llama_id}}", self.get_default_headers())
.add_path("llama_id", llama_id)
.add_query("limit", limit)
.add_query("sort_by", sort_by)
.add_query("tags", tags)
.serialize()
.set_method("GET")
)

Here we utilize the Serializer class to build the request URL with path parameters and query parameters effortlessly. Each parameter is serialized and incorporated into the request.

The Serializer class supports all serialization types described by OpenAPI specifications. This flexibility allows developers to seamlessly serialize URL components present in headers, cookies, path parameters, and query parameters in compliance with the OpenAPI standards. Whether it's form serialization, space-delimited serialization, pipe-delimited serialization, or deep object serialization, our Serializer class empowers developers to comply with the API specifications effortlessly.

The following example demonstrates how this functionality also support non-default serialization styles:

Serializer ...
.add_path("llama_id", llama_id, explode=True, style="matrix")
.add_path("status", status, explode=False, style="label")

By using a consistent and straightforward method for constructing requests, our Python SDKs v2 promotes simplicity and clarity, making it easier for developers to interact with the API and focus on their core objectives.

Validating the output

Special cases aren't special enough to break the rules.

After retrieving the response, we invoke the unmap function to parse the response into a model instance:

@cast_models
def get_something(
...

response = self.send_request(serialized_request)

return PaginatedSomethingResponse._unmap(response)

The unmap function attempts to deserialize the response into the corresponding model. However, if this process fails due to unexpected data format or missing fields, it raises an error.

By enforcing output validation, our Python SDK maintains consistency in data representation, making possible for our users to identify any misconceptions their might have about their data representations.

Mapping to Python

Readability counts.

The properties in your API do not always match the naming conventions in Python, and beyond that, they might be reserved words. To solve that problem our v2 Python SDKs have a mapping functionality.

To illustrate, let's check the following model definition:

@JsonMap(
{
"is_enabled": "isEnabled",
"something_list": "somethingList",
"user_role": "userRole"
}
)
class PaginatedSomethingResponse(BaseModel):
...

def __init__(
self,
page: int,
is_enabled: bool,
something_list: SomethingList,
user_role: Role,
org: OrgResponse = None,
):
self.page = page
self.is_enabled = is_enabled
self.something_list = self._define_list(something_list, SomethingList)
self.user_role = self._enum_matching(user_role, Role.list(), "role")
if org is not None:
self.org = self._define_object(org, OrgResponse)

Here, the @JsonMap annotation is employed to map keys from the Python representation to the corresponding values expected by the API. This ensures that the properties of the models align with Python’s naming conventions and the structure of the API data.

Descriptions and snippets

In the face of ambiguity, refuse the temptation to guess.

In addition to the powerful features introduced in our v2 Python SDKs, we've incorporated comprehensive descriptions and snippets, enhancing clarity and usability for developers.

The snippets can help developers to integrate SDK functionality into their projects with minimal effort. These snippets cover a wide range of SDK functionality and can be easily accessed from the README files generated with the SDK.

The following Snippet was generated for our Llama Store SDK:

from llama_store import LlamaStore, Environment
from llama_store.models import LlamaCreate

sdk = LlamaStore(
base_url=Environment.DEFAULT.value
)

request_body = LlamaCreate(**{
"name": "name",
"age": 2,
"color": "brown",
"rating": 3
})

result = sdk.llama.create_llama(request_body=request_body)

print(result)

We've also integrated reStructuredText (reST) docstrings into our SDKs. These docstrings serve as a rich source of documentation, providing detailed information about the SDK.

@cast_models
def get_something(
self,
llama_id: str,
limit: int,
sort_by: SortBy = None,
tags: List[float] = None,
) -> PaginatedSomethingResponse:
"""
Retrieve something based on specified parameters.

:param llama_id: The identifier of the something to retrieve.
:type llama_id: str
:param limit: The maximum number of results to return.
:type limit: int
:param sort_by: The sorting criteria for the results (optional).
:type sort_by: SortBy, optional
:param tags: The list of tags to filter the results (optional).
:type tags: List[float], optional
:return: Paginated response containing something data.
:rtype: PaginatedSomethingResponse
"""
...

The new format of docstrings is compatible with documentation generation tools like Sphinx.

Overall objectives/conclusion

Beautiful is better than ugly.

In conclusion, the v2 release of our Python SDKs embodies our core principles of generating human-friendly code and removing complexity from the user's hands. By prioritizing clarity and ease of use, developers can focus on their objectives without being bogged down by complex API interactions.

While maintaining support for essential features like authentication, retry, and refresh token handling out of the box, the v2 SDKs elevate the user experience with streamlined input handling, automated validation, and intuitive request building.

We invite you to experience the benefits of our new v2 SDKs by specifying "liblabVersion": "2" in the "python" customization section of your liblab config file:

{
"languages": [
"python"
],
"languageOptions": {
"python": {
"liblabVersion": "2"
}
}
}

Dive into simplified API interaction and unleash your productivity with our user-centric design approach.

Join our Discord community to share your questions, feedback, and experiences with the new SDKs. We look forward to hearing from you!