ServiceStack's Add ServiceStack Reference feature allows clients to generate Native Types from directly within PyCharm using ServiceStack IntelliJ Plugin - providing a simple way to give clients typed access to your ServiceStack Services.
First class development experience​
Python is one of the worlds most popular programming languages thanks to its ease of use and comprehensive libraries which sees it excels in many industries from education where it's often the first language taught in school to data science, machine learning and AI where it's often the dominant language used. To maximize the experience for calling ServiceStack APIs within these environments ServiceStack now supports Python as a 1st class Add ServiceStack Reference supported language which gives Python developers an end-to-end typed API for consuming ServiceStack APIs, complete with IDE integration in PyCharm as well as built-in support in x dotnet tool to generate Python DTOs for a remote ServiceStack instance from a single command-line.
Ideal idiomatic Typed Message-based API​
To maximize the utility of Python DTOs and enable richer tooling support, Python DTOs are generated as dataclasses with support for JSON serialization and annotated with Python 3 type hints - that's invaluable when scaling large Python code-bases and greatly improves discoverability of a remote API. DTOs are also enriched with interface markers through Python's multiple inheritance support which enables its optimal end-to-end typed API:
The Python DTOs and JsonServiceClient
library follow Python's PEP 8's naming conventions so they'll naturally fit into existing Python code bases. Here's a sample of techstacks.io generated Python DTOs containing string and int Enums, an example AutoQuery and a standard Request & Response DTO showcasing the rich typing annotations and naming conventions used:
class TechnologyTier(str, Enum):
PROGRAMMING_LANGUAGE = 'ProgrammingLanguage'
CLIENT = 'Client'
HTTP = 'Http'
SERVER = 'Server'
DATA = 'Data'
SOFTWARE_INFRASTRUCTURE = 'SoftwareInfrastructure'
OPERATING_SYSTEM = 'OperatingSystem'
HARDWARE_INFRASTRUCTURE = 'HardwareInfrastructure'
THIRD_PARTY_SERVICES = 'ThirdPartyServices'
class Frequency(IntEnum):
DAILY = 1
WEEKLY = 7
MONTHLY = 30
QUARTERLY = 90
# @Route("/technology/search")
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
@dataclass
class FindTechnologies(QueryDb2[Technology, TechnologyView], IReturn[QueryResponse[TechnologyView]], IGet):
ids: Optional[List[int]] = None
name: Optional[str] = None
vendor_name: Optional[str] = None
name_contains: Optional[str] = None
vendor_name_contains: Optional[str] = None
description_contains: Optional[str] = None
# @Route("/orgs/{Id}", "PUT")
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
@dataclass
class UpdateOrganization(IReturn[UpdateOrganizationResponse], IPut):
id: int = 0
slug: Optional[str] = None
name: Optional[str] = None
description: Optional[str] = None
color: Optional[str] = None
text_color: Optional[str] = None
link_color: Optional[str] = None
background_color: Optional[str] = None
background_url: Optional[str] = None
logo_url: Optional[str] = None
hero_url: Optional[str] = None
lang: Optional[str] = None
delete_posts_with_report_count: int = 0
disable_invites: Optional[bool] = None
default_post_type: Optional[str] = None
default_subscription_post_types: Optional[List[str]] = None
post_types: Optional[List[str]] = None
moderator_post_types: Optional[List[str]] = None
technology_ids: Optional[List[int]] = None
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
@dataclass
class UpdateOrganizationResponse:
response_status: Optional[ResponseStatus] = None
The smart Python JsonServiceClient
available in the servicestack pip and conda packages enabling the same
productive, typed API development experience available in our other 1st-class supported client platforms.
Using dataclasses enables DTOs to be populated using a single constructor expression utilizing named parameters which together with the generic JsonServiceClient
enables end-to-end typed API Requests in a single LOC:
from .dtos import *
from servicestack import JsonServiceClient
client = JsonServiceClient("https://test.servicestack.net")
response: HelloResponse = client.get(Hello(name="World"))
INFO
The HelloResponse
optional type hint doesn't change runtime behavior but enables static analysis tools and IDEs like PyCharm to provide rich intelli-sense and development time feedback
Installation​
The only requirements for Python apps to perform typed API Requests are the generated Python DTOs and the generic JsonServiceClient
which can be installed globally or in a virtual Python environment using Python's pip:
pip install servicestack
Or if preferred can be installed with conda:
conda install conda-build.
- Add conda-forge as channel using
conda config --add channels conda-forge
- On root directory run
conda build .
PyCharm ServiceStack Plugin​
Python developers of PyCharm Professional and FREE Community Edition can get a simplified development experience for consuming ServiceStack Services by installing the ServiceStack Plugin from the JetBrains Marketplace:
Where you'll be able to right-click on a directory and click on ServiceStack Reference on the context menu:
To launch the Add Python ServiceStack Reference dialog where you can enter the remote URL of the ServiceStack endpoint you wish to call to generate the Typed Python DTOs for all APIs which by default will saved to dtos.py
:
Then just import the DTOs and JsonServiceClient
to be able to consume any of the remote ServiceStack APIs:
from .dtos import *
from servicestack import JsonServiceClient
client = JsonServiceClient("https://techstacks.io")
response = client.send(FindTechnologies(
ids=[1, 2, 4, 6],
vendor_name="Google",
take=10))
If any of the the remote APIs change their DTOs can be updated by right-clicking on dtos.py
and clicking Update ServiceStack Reference:
Simple command-line utility for Python​
Developers using other Python IDEs and Text Editors like VS Code can utilize the cross-platform x
command line utility for generating Python DTOs from the command-line.
To install first install the latest .NET SDK for your OS then install the x
dotnet tool with:
dotnet tool install --global x
Adding a ServiceStack Reference​
To Add a Python ServiceStack Reference just call x python
with the URL of a remote ServiceStack instance:
x python https://techstacks.io
Result:
Saved to: dtos.py
Calling x python
with just a URL will save the DTOs using the Host name, you can override this by specifying a FileName as the 2nd argument:
x python https://techstacks.io Tech
Result:
Saved to: Tech.dtos.py
Updating a ServiceStack Reference​
To Update an existing ServiceStack Reference, call x python
with the Filename:
x python dtos.py
Result:
Updated: dtos.py
Which will update the File with the latest Python Server DTOs from techstacks.io. You can also customize how DTOs are generated by uncommenting the Python DTO Customization Options and updating them again.
Updating all Python DTOs​
Calling x python
without any arguments will update all Python DTOs in the current directory:
x python
Result:
Updated: Tech.dtos.py
Updated: dtos.py
Smart Generic JsonServiceClient​
The generic JsonServiceClient
is a 1st class client with the same rich featureset of the smart ServiceClients in other 1st class supported languages sporting a terse, typed flexible API with support for additional untyped params, custom URLs and HTTP Methods, dynamic response types including consuming API responses in raw text and binary data formats. Clients can be decorated to support generic functionality using instance and static Request, Response and Exception Filters.
It includes built-in support for a number of ServiceStack Auth options including HTTP Basic Auth and stateless Bearer Token Auth Providers like API Key and JWT Auth as well as stateful Sessions used by the popular credentials Auth Provider and an on_authentication_required
callback for enabling custom authentication methods. The built-in auth options include auto-retry support for transparently authenticating and retrying authentication required requests as well as Refresh Token Cookie support where it will transparently fetch new JWT Bearer Tokens automatically behind-the-scenes for friction-less stateless JWT support.
A snapshot of these above features is captured in the high-level public API below:
class JsonServiceClient:
base_url: str
reply_base_url: str
oneway_base_url: str
headers: Optional[Dict[str, str]]
bearer_token: Optional[str]
refresh_token: Optional[str]
refresh_token_uri: Optional[str]
username: Optional[str]
password: Optional[str]
on_authentication_required: Callable[[], None]
global_request_filter: Callable[[SendContext], None] # static
request_filter: Callable[[SendContext], None]
global_response_filter: Callable[[Response], None] # static
response_filter: Callable[[Response], None]
exception_filter: Callable[[Response, Exception], None]
global_exception_filter: Callable[[Response, Exception], None]
def __init__(self, base_url)
def set_credentials(self, username, password)
@property def token_cookie(self)
@property def refresh_token_cookie(self)
def get(self, request: IReturn[T], args: Dict[str, Any] = None) -> T
def post(self, request: IReturn[T], body: Any = None, args: Dict[str, Any] = None) -> T
def put(self, request: IReturn[T], body: Any = None, args: Dict[str, Any] = None) -> T
def patch(self, request: IReturn[T], body: Any = None, args: Dict[str, Any] = None) -> T
def delete(self, request: IReturn[T], args: Dict[str, Any] = None) -> T
def options(self, request: IReturn[T], args: Dict[str, Any] = None) -> T
def head(self, request: IReturn[T], args: Dict[str, Any] = None) -> T
def send(self, request, method: Any = None, body: Any = None, args: Dict[str, Any] = None)
def get_url(self, path: str, response_as: Type, args: Dict[str, Any] = None)
def delete_url(self, path: str, response_as: Type, args: Dict[str, Any] = None)
def options_url(self, path: str, response_as: Type, args: Dict[str, Any] = None)
def head_url(self, path: str, response_as: Type, args: Dict[str, Any] = None)
def post_url(self, path: str, body: Any = None, response_as: Type = None, args: Dict[str, Any] = None)
def put_url(self, path: str, body: Any = None, response_as: Type = None, args: Dict[str, Any] = None)
def patch_url(self, path: str, body: Any = None, response_as: Type = None, args: Dict[str, Any] = None)
def send_url(self, path: str, method: str = None, response_as: Type = None, body: Any = None,
args: Dict[str, Any] = None)
def send_all(self, request_dtos: List[IReturn[T]]) # Auto Batch Reply Requests
def send_all_oneway(self, request_dtos: list) # Auto Batch Oneway Requests
Change Default Server Configuration​
The above defaults are also overridable on the ServiceStack Server by modifying the default config on the NativeTypesFeature
Plugin, e.g:
var nativeTypes = this.GetPlugin<NativeTypesFeature>();
nativeTypes.MetadataTypesConfig.AddResponseStatus = true;
...
Python specific functionality can be added by PythonGenerator
PythonGenerator.DefaultImports.Add("requests");
Customize DTO Type generation​
Additional Python specific customization can be statically configured like PreTypeFilter
, InnerTypeFilter
& PostTypeFilter
(available in all languages) can be used to inject custom code in the generated DTOs output.
Use the PreTypeFilter
to generate source code before and after a Type definition, e.g. this will append the Decorator
class decorator on non enum & interface types:
PythonGenerator.PreTypeFilter = (sb, type) => {
if (!type.IsEnum.GetValueOrDefault() && !type.IsInterface.GetValueOrDefault())
{
sb.AppendLine("@Decorator");
}
};
The InnerTypeFilter
gets invoked just after the Type Definition which can be used to generate common members for all Types and interfaces, e.g:
PythonGenerator.InnerTypeFilter = (sb, type) => {
sb.AppendLine("id:str = str(random.random())[2:]");
};
There's also PrePropertyFilter
& PostPropertyFilter
for generating source before and after properties, e.g:
PythonGenerator.PrePropertyFilter = (sb , prop, type) => {
if (prop.Name == "Id")
{
sb.AppendLine("@IsInt");
}
};
Emit custom code​
To enable greater flexibility when generating complex Typed DTOs, you can use [Emit{Language}]
attributes to generate code before each type or property.
These attributes can be used to generate different attributes or annotations to enable client validation for different validation libraries in different languages, e.g:
[EmitCode(Lang.Python, "# App User")]
[EmitPython("@Validate")]
public class User : IReturn<User>
{
[EmitPython("@IsNotEmpty", "@IsEmail")]
[EmitCode(Lang.Swift | Lang.Dart, new[]{ "@isNotEmpty()", "@isEmail()" })]
public string Email { get; set; }
}
Which will generate [EmitPython]
code in Python DTOs:
# App User
@Validate
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
@dataclass
class User:
@IsNotEmpty
@IsEmail
email: Optional[str] = None
Whilst the generic [EmitCode]
attribute lets you emit the same code in multiple languages with the same syntax.
Python Reference Example​
Lets walk through a simple example to see how we can use ServiceStack's Python DTO annotations in our Python JsonServiceClient. Firstly we'll need to add a Python Reference to the remote ServiceStack Service by right-clicking on a project folder and clicking on ServiceStack Reference...
(as seen in the above screenshot).
This will import the remote Services dtos into your local project which looks similar to:
""" Options:
Date: 2021-08-14 15:33:39
Version: 5.111
Tip: To override a DTO option, remove "//" prefix before updating
BaseUrl: https://techstacks.io
#GlobalNamespace:
#MakePropertiesOptional: False
#AddServiceStackTypes: True
#AddResponseStatus: False
#AddImplicitVersion:
#AddDescriptionAsComments: True
#IncludeTypes:
#ExcludeTypes:
#DefaultImports: datetime,decimal,marshmallow.fields:*,servicestack:*,typing:*,dataclasses:dataclass/field,dataclasses_json:dataclass_json/LetterCase/Undefined/config,enum:Enum/IntEnum
#DataClass:
#DataClassJson:
"""
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
@dataclass
class GetTechnologyResponse:
created: datetime.datetime = datetime.datetime(1, 1, 1)
technology: Optional[Technology] = None
technology_stacks: Optional[List[TechnologyStack]] = None
response_status: Optional[ResponseStatus] = None
# @Route("/technology/{Slug}")
@dataclass_json(letter_case=LetterCase.CAMEL, undefined=Undefined.EXCLUDE)
@dataclass
class GetTechnology(IReturn[GetTechnologyResponse], IRegisterStats, IGet):
slug: Optional[str] = None
In keeping with idiomatic style of local .py
sources, generated types are not wrapped within a module by default. This lets you reference the types you want directly using normal import destructuring syntax:
from .dtos import GetTechnology, GetTechnologyResponse
Or import all Types into your preferred variable namespace with:
from .dtos import *
request = GetTechnology()
Making Typed API Requests​
Making API Requests in Python is the same as all other ServiceStack's Service Clients by sending a populated Request DTO using a JsonServiceClient
which returns typed Response DTO.
So the only things we need to make any API Request is the JsonServiceClient
from the servicestack
package and any DTO's we're using from generated Python ServiceStack Reference, e.g:
from .dtos import GetTechnology, GetTechnologyResponse
from servicestack import JsonServiceClient
client = JsonServiceClient("https://techstacks.io")
request = GetTechnology()
request.slug = "ServiceStack"
r: GetTechnologyResponse = client.get(request) # typed to GetTechnologyResponse
tech = r.technology # typed to Technology
print(f"{tech.name} by {tech.vendor_name} ({tech.product_url})")
print(f"`{tech.name} TechStacks: {r.technology_stacks}")
Constructors Initializer​
All Python Reference dataclass DTOs also implements init making them much nicer to populate using a constructor expression with named params syntax we're used to in C#, so instead of:
request = Authenticate()
request.provider = "credentials"
request.user_name = user_name
request.password = password
request.remember_me = remember_me
response = client.post(request)
You can populate DTOs with a single constructor expression without any loss of Python's Typing benefits:
response = client.post(Authenticate(
provider='credentials',
user_name=user_name,
password=password,
remember_me=remember_me))
Sending additional arguments with Typed API Requests​
Many AutoQuery Services utilize implicit conventions to query fields that aren't explicitly defined on AutoQuery Request DTOs, these can be queried by specifying additional arguments with the typed Request DTO, e.g:
request = FindTechStacks()
r:QueryResponse[TechnologyStackView] = client.get(request, args={"vendorName": "ServiceStack"})
Making API Requests with URLs​
In addition to making Typed API Requests you can also call Services using relative or absolute urls, e.g:
client.get("/technology/ServiceStack", response_as=GetTechnologyResponse)
client.get("https://techstacks.io/technology/ServiceStack", response_as=GetTechnologyResponse)
# https://techstacks.io/technology?Slug=ServiceStack
args = {"slug":"ServiceStack"}
client.get("/technology", args=args, response_as=GetTechnologyResponse)
as well as POST Request DTOs to custom urls:
client.post_url("/custom-path", request, args={"slug":"ServiceStack"})
client.post_url("http://example.org/custom-path", request)
Raw Data Responses​
The JsonServiceClient
also supports Raw Data responses like string
and byte[]
which also get a Typed API once declared on Request DTOs using the IReturn<T>
marker:
public class ReturnString : IReturn<string> {}
public class ReturnBytes : IReturn<byte[]> {}
Which can then be accessed as normal, with their Response typed to a JavaScript str
or bytes
for raw byte[]
responses:
str:str = client.get(ReturnString())
data:bytes = client.get(ReturnBytes())
Authenticating using Basic Auth​
Basic Auth support is implemented in JsonServiceClient
and follows the same API made available in the C# Service Clients where the userName/password
properties can be set individually, e.g:
client = JsonServiceClient(baseUrl)
client.username = user
client.password = pass
response = client.get(SecureRequest())
Or use client.set_credentials()
to have them set both together.
Authenticating using Credentials​
Alternatively you can authenticate using userName/password credentials by
adding a Python Reference
to your remote ServiceStack Instance and sending a populated Authenticate
Request DTO, e.g:
request = Authenticate()
request.provider = "credentials"
request.user_name = user_name
request.password = password
request.remember_me = true
response:AuthenticateResponse = client.post(request)
This will populate the JsonServiceClient
with Session Cookies
which will transparently be sent on subsequent requests to make authenticated requests.
Authenticating using JWT​
Use the bearer_token
property to Authenticate with a ServiceStack JWT Provider using a JWT Token:
client.bearer_token = jwt
Alternatively you can use just a Refresh Token instead:
client.refresh_token = refresh_token
Where the client will automatically fetch a new JWT Bearer Token using the Refresh Token for authenticated requests.
Authenticating using an API Key​
Use the bearer_token
property to Authenticate with an API Key:
client.bearer_token = api_key
Transparently handle 401 Unauthorized Responses​
If the server returns a 401 Unauthorized Response either because the client was Unauthenticated or the
configured Bearer Token or API Key used had expired or was invalidated, you can use onAuthenticationRequired
callback to re-configure the client before automatically retrying the original request, e.g:
auth_client = JsonServiceClient(AUTH_URL)
client.on_authentication_required = lambda c=client, a=auth_client: [
a.set_credentials(username, password),
client.set_bearer_token(cast(AuthenticateResponse, a.get(Authenticate())).bearer_token)
]
# Automatically retries requests returning 401 Responses with new bearerToken
response = client.get(Secured())
Automatically refresh Access Tokens​
With the Refresh Token support in JWT
you can use the refresh_token
property to instruct the Service Client to automatically fetch new JWT Tokens behind the scenes before automatically retrying failed requests due to invalid or expired JWTs, e.g:
# Authenticate to get new Refresh Token
auth_client = JsonServiceClient(AUTH_URL)
auth_client.username = username
auth_client.password = password
auth_response = auth_client.get(Authenticate())
# Configure client with RefreshToken
client.refresh_token = auth_response.refresh_token
# Call authenticated Services and clients will automatically retrieve new JWT Tokens as needed
response = client.get(Secured())
Use the refresh_token_uri
property when refresh tokens need to be sent to a different ServiceStack Server, e.g:
client.refresh_token = refresh_token
client.refresh_token_uri = AUTH_URL + "/access-token"
DTO Customization Options​
In most cases you'll just use the generated Python DTO's as-is, however you can further customize how the DTO's are generated by overriding the default options.
The header in the generated DTO's show the different options Python native types support with their defaults. Default values are shown with the comment prefix of //
. To override a value, remove the #
and specify the value to the right of the :
. Any uncommented value will be sent to the server to override any server defaults.
The DTO comments allows for customizations for how DTOs are generated. The default options that were used to generate the DTO's are repeated in the header comments of the generated DTOs, options that are preceded by a Python comment #
are defaults from the server, any uncommented value will be sent to the server
to override any server defaults.
""" Options:
Date: 2021-08-15 08:26:46
Version: 5.111
Tip: To override a DTO option, remove "#" prefix before updating
BaseUrl: https://techstacks.io
#GlobalNamespace:
#MakePropertiesOptional: False
#AddServiceStackTypes: True
#AddResponseStatus: False
#AddImplicitVersion:
#AddDescriptionAsComments: True
#IncludeTypes:
#ExcludeTypes:
#DefaultImports: datetime,decimal,marshmallow.fields:*,servicestack:*,typing:*,dataclasses:dataclass/field,dataclasses_json:dataclass_json/LetterCase/Undefined/config,enum:Enum/IntEnum
#DataClass:
#DataClassJson:
"""
We'll go through and cover each of the above options to see how they affect the generated DTO's:
Change Default Server Configuration​
The above defaults are also overridable on the ServiceStack Server by modifying the default config on the NativeTypesFeature
Plugin, e.g:
//Server example in C#
var nativeTypes = this.GetPlugin<NativeTypesFeature>();
nativeTypes.MetadataTypesConfig.AddResponseStatus = true;
...
We'll go through and cover each of the above options to see how they affect the generated DTO's:
GlobalNamespace​
As Python lacks the concept of namespaces this just emits a comment with the namespace name:
# module dtos
AddResponseStatus​
Automatically add a response_status
property on all Response DTO's, regardless if it wasn't already defined:
class GetTechnologyResponse:
...
response_status: Optional[ResponseStatus] = None
AddImplicitVersion​
Lets you specify the Version number to be automatically populated in all Request DTO's sent from the client:
class GetTechnology(IReturn[GetTechnologyResponse], IRegisterStats, IGet):
version: int = 1
...
This lets you know what Version of the Service Contract that existing clients are using making it easy to implement ServiceStack's recommended versioning strategy.
IncludeTypes​
Is used as a Whitelist to specify only the types you would like to have code-generated:
""" Options:
IncludeTypes: GetTechnology,GetTechnologyResponse
Will only generate GetTechnology
and GetTechnologyResponse
DTO's:
class GetTechnologyResponse:
...
class GetTechnology:
...
Include Generic Types​
Use .NET's Type Name to include Generic Types, i.e. the Type name separated by the backtick followed by the number of generic arguments, e.g:
IncludeTypes: IReturn`1,MyPair`2
Include Request DTO and its dependent types​
You can include a Request DTO and all its dependent types with a .*
suffix on the Request DTO, e.g:
""" Options:
IncludeTypes: GetTechnology.*
Which will include the GetTechnology
Request DTO, the GetTechnologyResponse
Response DTO and all Types that they both reference.
Include All Types within a C# namespace​
If your DTOs are grouped into different namespaces they can be all included using the /*
suffix, e.g:
""" Options:
IncludeTypes: MyApp.ServiceModel.Admin/*
This will include all DTOs within the MyApp.ServiceModel.Admin
C# namespace.
Include All Services in a Tag Group​
Services grouped by Tag can be used in the IncludeTypes
where tags can be specified using braces in the format {tag}
or {tag1,tag2,tag3}
, e.g:
""" Options:
IncludeTypes: {web,mobile}
Or individually:
""" Options:
IncludeTypes: {web},{mobile}
ExcludeTypes​
Is used as a Blacklist to specify which types you would like excluded from being generated:
""" Options:
ExcludeTypes: GetTechnology,GetTechnologyResponse
Will exclude GetTechnology
and GetTechnologyResponse
DTOs from being generated.
DefaultImports​
The module:Symbols
short-hand syntax can be used for specifying additional imports in your generated Python DTO. There are 3 different syntaxes for specifying different Python imports:
""" Options:
...
DefaultImports: datetime,typing:*,enum:Enum/IntEnum
"""
Which will generate the popular import form of:
import datetime
from typing import *
from enum import Enum, IntEnum
DataClass​
Change what dataclass
decorator is used, e.g:
""" Options:
...
DataClass: init=False
"""
Will decorate every DTO with:
@dataclass(init=False)
class GetTechnology(IReturn[GetTechnologyResponse], IRegisterStats, IGet):
slug: Optional[str] = None
DataClassJson​
Change what dataclass_json
decorator is used, e.g:
""" Options:
...
DataClassJson: letter_case=LetterCase.PASCAL
"""
Will decorate every DTO with:
@dataclass_json(letter_case=LetterCase.PASCAL)
class GetTechnology(IReturn[GetTechnologyResponse], IRegisterStats, IGet):
slug: Optional[str] = None
Which will result in each type being serialized with PascalCase.
Customize Serialization​
The servicestack
client lib allows for flexible serialization customization where you can change how different .NET Types are serialized and deserialized into native Python types.
To illustrate this we'll walk through how serialization of properties containing binary data to Base64 is implemented.
First we specify the Python DTO generator to emit bytes
type hint for the popular .NET binary data types:
PythonGenerator.TypeAliases[typeof(byte[]).Name] = "bytes";
PythonGenerator.TypeAliases[typeof(Stream).Name] = "bytes";
In the Python app we can then specify the serializers and deserializers to use for deserializing properties with the bytes
data type which converts binary data to/from Base64:
from servicestack import TypeConverters
def to_bytearray(value: Optional[bytes]):
if value is None:
return None
return base64.b64encode(value).decode('ascii')
def from_bytearray(base64str: Optional[str]):
return base64.b64decode(base64str)
TypeConverters.serializers[bytes] = to_bytearray
TypeConverters.deserializers[bytes] = from_bytearray
Inspect Utils​
To help clients with inspecting API Responses the servicestack
library also includes a number of helpful utils to quickly visualizing API outputs.
For a basic indented object graph you can use dump
to capture and printdump
to print the output of any API Response, e.g:
from dataclasses import dataclass
from dataclasses_json import dataclass_json, Undefined
from typing import Optional
from servicestack import printdump, printtable
@dataclass_json(undefined=Undefined.EXCLUDE)
@dataclass
class GithubRepo:
name: str
description: Optional[str] = None
homepage: Optional[str] = None
lang: Optional[str] = field(metadata=config(field_name="language"),default=None)
watchers: Optional[int] = 0
forks: Optional[int] = 0
response = requests.get(f'https://api.github.com/orgs/python/repos')
orgRepos = GithubRepo.schema().loads(response.text, many=True)
orgRepos.sort(key=operator.attrgetter('watchers'), reverse=True)
print(f'Top 3 {orgName} Repos:')
printdump(orgRepos[0:3])
Output:
Top 3 python Repos:
[
{
name: mypy,
description: Optional static typing for Python 3 and 2 (PEP 484),
homepage: http://www.mypy-lang.org/,
lang: Python,
watchers: 9638,
forks: 1564
},
{
name: peps,
description: Python Enhancement Proposals,
homepage: https://www.python.org/dev/peps/,
lang: Python,
watchers: 2459,
forks: 921
},
{
name: typeshed,
description: Collection of library stubs for Python, with static types,
homepage: ,
lang: Python,
watchers: 1942,
forks: 972
}
]
For tabular resultsets you can use table
to capture and printtable
to print API resultsets in a human-friendly markdown table with an optional headers
parameter to specify the order and columns to display, e.g:
print(f'\nTop 10 {orgName} Repos:')
printtable(orgRepos[0:10],headers=['name','lang','watchers','forks'])
Output:
Top 10 python Repos:
+--------------+-----------+------------+---------+
| name | lang | watchers | forks |
|--------------+-----------+------------+---------|
| mypy | Python | 9638 | 1564 |
| peps | Python | 2459 | 921 |
| typeshed | Python | 1942 | 972 |
| pythondotorg | Python | 1038 | 432 |
| asyncio | | 945 | 178 |
| typing | Python | 840 | 130 |
| raspberryio | Python | 217 | 38 |
| typed_ast | C | 171 | 43 |
| planet | Python | 100 | 145 |
| psf-salt | SaltStack | 87 | 50 |
+--------------+-----------+------------+---------+
Alternatively you can use htmldump
to generate API responses in a HTML UI which is especially useful in Python Jupyter Notebooks to easily visualize API responses, e.g: