azure-sdk/docs/python/implementation.md

21 KiB

title keywords permalink folder sidebar
Python Guidelines: Implementation guidelines python python_implementation.html python python_sidebar

Configuration

{% include requirement/MUST id="python-envvars-global" %} honor the following environment variables for global configuration settings:

{% include tables/environment_variables.md %}

Parameter validation

The service client will have several methods that send requests to the service. Service parameters are directly passed across the wire to an Azure service. Client parameters aren't passed directly to the service, but used within the client library to fulfill the request. Parameters that are used to construct a URI, or a file to be uploaded are examples of client parameters.

{% include requirement/MUST id="python-params-client-validation" %} validate client parameters. Validation is especially important for parameters used to build up the URL since a malformed URL means that the client library will end up calling an incorrect endpoint.

# No:
def get_thing(name: "str") -> "str":
    url = f'https://<host>/things/{name}'
    return requests.get(url).json()

try:
    thing = get_thing('') # Ooops - we will end up calling '/things/' which usually lists 'things'. We wanted a specific 'thing'.
except ValueError:
    print('We called with some invalid parameters. We should fix that.')

# Yes:
def get_thing(name: "str") -> "str":
    if not name:
        raise ValueError('name must be a non-empty string')
    url = f'https://<host>/things/{name}'
    return requests.get(url).json()

try:
    thing = get_thing('')
except ValueError:
    print('We called with some invalid parameters. We should fix that.')

{% include requirement/MUSTNOT id="python-params-service-validation" %} validate service parameters. Don't do null checks, empty string checks, or other common validating conditions on service parameters. Let the service validate all request parameters.

{% include requirement/MUST id="python-params-devex" %} validate the developer experience when the service parameters are invalid to ensure appropriate error messages are generated by the service. Work with the service team if the developer experience is compromised because of service-side error messages.

Network operations

Since the client library generally wraps one or more HTTP requests, it's important to support standard network capabilities. Although not widely understood, asynchronous programming techniques are essential in developing resilient web services. Many developers prefer synchronous method calls for their easy semantics when learning how to use a technology. The HTTP pipeline is a component in the azure-core library that assists in providing connectivity to HTTP-based Azure services.

{% include requirement/MUST id="python-network-http-pipeline" %} use the [HTTP pipeline] to send requests to service REST endpoints.

{% include requirement/SHOULD id="python-network-use-policies" %} include the following policies in the HTTP pipeline:

  • Telemetry (azure.core.pipeline.policies.UserAgentPolicy)
  • Unique Request ID
  • Retry (azure.core.pipeline.policies.RetryPolicy and azure.core.pipeline.policies.AsyncRetryPolicy)
  • Credentials
  • Response downloader
  • Distributed tracing
  • Logging (azure.core.pipeline.policies.NetworkTraceLoggingPolicy)

Dependencies

{% include requirement/MUST id="python-dependencies-approved-list" %} only pick external dependencies from the following list of well known packages for shared functionality:

{% include_relative approved_dependencies.md %}

{% include requirement/MUSTNOT id="python-dependencies-external" %} use external dependencies outside the list of well known dependencies. To get a new dependency added, contact the [Architecture Board].

{% include requirement/MUSTNOT id="python-dependencies-vendor" %} vendor dependencies unless approved by the [Architecture Board].

When you vendor a dependency in Python, you include the source from another package as if it was part of your package.

{% include requirement/MUSTNOT id="python-dependencies-pin-version" %} pin a specific version of a dependency unless that is the only way to work around a bug in said dependencies versioning scheme.

Applications are expected to pin exact dependencies. Libraries aren't. A library should use a compatible release identifier for the dependency.

Service-specific common library code

There are occasions when common code needs to be shared between several client libraries. For example, a set of cooperating client libraries may wish to share a set of exceptions or models.

{% include requirement/MUST id="python-commonlib-approval" %} gain [Architecture Board] approval prior to implementing a common library.

{% include requirement/MUST id="python-commonlib-minimize-code" %} minimize the code within a common library. Code within the common library is available to the consumer of the client library and shared by multiple client libraries within the same namespace.

{% include requirement/MUST id="python-commonlib-namespace" %} store the common library in the same namespace as the associated client libraries.

A common library will only be approved if:

  • The consumer of the non-shared library will consume the objects within the common library directly, AND
  • The information will be shared between multiple client libraries.

Let's take two examples:

  1. Implementing two Cognitive Services client libraries, we find a model is required that is produced by one Cognitive Services client library and consumed by another Coginitive Services client library, or the same model is produced by two client libraries. The consumer is required to do the passing of the model in their code, or may need to compare the model produced by one client library vs. that produced by another client library. This is a good candidate for choosing a common library.

  2. Two Cognitive Services client libraries throw an ObjectNotFound exception to indicate that an object was not detected in an image. The user might trap the exception, but otherwise will not operate on the exception. There is no linkage between the ObjectNotFound exception in each client library. This is not a good candidate for creation of a common library (although you may wish to place this exception in a common library if one exists for the namespace already). Instead, produce two different exceptions - one in each client library.

Error handling

{% include requirement/MUST id="python-errors-exceptions" %} raise an exception if a method fails to perform its intended functionality. Don't return None or a boolean to indicate errors.

# Yes
try:
    resource = client.create_resource(name)
except azure.core.errors.ResourceExistsException:
    print('Failed - we need to fix this!')

# No
resource = client.create_resource(name):
if not resource:
    print('Failed - we need to fix this!')

{% include requirement/MUSTNOT id="python-errors-normal-responses" %} throw an exception for "normal responses".

Consider an exists method. The method must distinguish between the service returned a client error 404/NotFound and a failure to even make a request:

# Yes
try:
    exists = client.resource_exists(name):
    if not exists:
        print("The resource doesn't exist...")
except azure.core.errors.ServiceRequestError:
    print("We don't know if the resource exists - so it was appropriate to throw an exception!")

# No
try:
    client.resource_exists(name)
except azure.core.errors.ResourceNotFoundException:
    print("The resource doesn't exist... but that shouldn't be an exceptional case for an 'exists' method")

{% include requirement/SHOULDNOT id="python-errors-new-exceptions" %} create a new exception type unless the developer can remediate the error by doing something different. Specialized exception types should be based on existing exception types present in the azure-core package.

{% include requirement/MUST id="python-errors-on-http-request-failed" %} produce an error when an HTTP request fails with an unsuccessful HTTP status code (as defined by the service).

{% include requirement/MUST id="python-errors-include-request-response" %} include the HTTP response (status code and headers) and originating request (URL, query parameters, and headers) in the exception.

For higher-level methods that use multiple HTTP requests, either the last exception or an aggregate exception of all failures should be produced.

{% include requirement/MUST id="python-errors-rich-info" %} include any service-specific error information in the exception. Service-specific error information must be available in service-specific properties or fields.

{% include requirement/MUST id="python-errors-documentation" %} document the errors that are produced by each method. Don't document commonly thrown errors that wouldn't normally be documented in Python.

{% include requirement/MUSTNOT id="python-errors-use-standard-exceptions" %} create new exception types when a built-in exception type will suffice.

{% include requirement/MUST id="python-errors-use-chaining" %} allow exception chaining to include the original source of the error.

# Yes:
try:
    # do something
except:
    raise MyOwnErrorWithNoContext()

# No:
success = True
try:
    # do something
except:
    success = False
if not success:
    raise MyOwnErrorWithNoContext()

# No:
success = True
try:
    # do something
except:
    raise MyOwnErrorWithNoContext() from None

Logging

{% include requirement/MUST id="python-logging-usage" %} use Pythons standard logging module.

{% include requirement/MUST id="python-logging-nameed-logger" %} provide a named logger for your library.

The logger for your package must use the name of the module. The library may provide additional child loggers. If child loggers are provided, document them.

For example:

  • Package name: azure-someservice
  • Module name: azure.someservice
  • Logger name: azure.someservice
  • Child logger: azure.someservice.achild

These naming rules allow the consumer to enable logging for all Azure libraries, a specific client library, or a subset of a client library.

{% include requirement/MUST id="python-logging-error" %} use the ERROR logging level for failures where it's unlikely the application will recover (for example, out of memory).

{% include requirement/MUST id="python-logging-warn" %} use the WARNING logging level when a function fails to perform its intended task. The function should also raise an exception.

Don't include occurrences of self-healing events (for example, when a request will be automatically retried).

{% include requirement/MUST id="python-logging-info" %} use the INFO logging level when a function operates normally.

{% include requirement/MUST id="python-logging-debug" %} use the DEBUG logging level for detailed trouble shooting scenarios.

The DEBUG logging level is intended for developers or system administrators to diagnose specific failures.

{% include requirement/MUSTNOT id="python-logging-sensitive-info" %} send sensitive information in log levels other than DEBUG. For example, redact or remove account keys when logging headers.

{% include requirement/MUST id="python-logging-request" %} log the request line, response line, and headers for an outgoing request as an INFO message.

{% include requirement/MUST id="python-logging-cancellation" %} log an INFO message, if a service call is canceled.

{% include requirement/MUST id="python-logging-exceptions" %} log exceptions thrown as a WARNING level message. If the log level set to DEBUG, append stack trace information to the message.

You can determine the logging level for a given logger by calling logging.Logger.isEnabledFor.

Distributed tracing

DRAFT section

{% include requirement/MUST id="python-tracing-span-per-method" %} create a new trace span for each library method invocation. The easiest way to do so is by adding the distributed tracing decorator from azure.core.tracing.

{% include requirement/MUST id="python-tracing-span-name" %} use <package name>/<method name> as the name of the span.

{% include requirement/MUST id="python-tracing-span-per-call" %} create a new span for each outgoing network call. If using the HTTP pipeline, the new span is created for you.

{% include requirement/MUST id="python-tracing-propagate" %} propagate tracing context on each outgoing service request.

Azure Core

The azure-core package provides common functionality for client libraries. Documentation and usage examples can be found in the [azure/azure-sdk-for-python] repository.

HTTP pipeline

The HTTP pipeline is an HTTP transport that is wrapped by multiple policies. Each policy is a control point that can modify either the request or response. A default set of policies is provided to standardize how client libraries interact with Azure services.

For more information on the Python implementation of the pipeline, see the documentation.

Custom Policies

Some services may require custom policies to be implemented. For example, custom policies may implement fall back to secondary endpoints during retry, request signing, or other specialized authentication techniques.

{% include requirement/SHOULD id="python-pipeline-core-policies" %} use the policy implementations in azure-core whenever possible.

{% include requirement/MUST id="python-pipeline-document-policies" %} document any custom policies in your package. The documentation should make it clear how a user of your library is supposed to use the policy.

{% include requirement/MUST id="python-pipeline-policy-namespace" %} add the policies to the azure.<package name>.pipeline.policies namespace.

Protocols

LROPoller

T = TypeVar("T")
class LROPoller(Protocol):

    def result(self, timeout=None) -> T:
        """ Retreive the final result of the long running operation.

        :param timeout: How long to wait for operation to complete (in seconds). If not specified, there is no timeout.
        :raises TimeoutException: If the operation has not completed before it timed out.
        """
        ...

    def wait(self, timeout=None) -> None:
        """ Wait for the operation to complete.

        :param timeout: How long to wait for operation to complete (in seconds). If not specified, there is no timeout.
        """
    
    def done(self) -> boolean:
        """ Check if long running operation has completed. 
        """

    def add_done_callback(self, func) -> None:
        """ Register callback to be invoked when operation completes.

        :param func: Callable that will be called with the eventual result ('T') of the operation.
        """
        ...

azure.core.LROPoller implements the LROPoller protocol.

Paged

T = TypeVar("T")
class ByPagePaged(Protocol, Iterable[Iterable[T]]):
    continuation_token: "str"

class Paged(Protocol, Iterable[T]):
    continuation_token: "str"

    def by_page(self) -> ByPagePaged[T] ...

azure.core.Paged implements the Paged protocol.

DiagnosticsResponseHook

class ResponseHook(Protocol):
    
    __call__(self, headers, deserialized_response): -> None ...

Versioning

{% include requirement/MUST id="python-versioning-semver" %} use semantic versioning for your package.

{% include requirement/MUST id="python-versioning-beta" %} use the bN pre-release segment for preview releases.

Don't use pre-release segments other than the ones defined in PEP440 (aN, bN, rcN). Build tools, publication tools, and index servers may not sort the versions correctly.

{% include requirement/MUST id="python-versioning-changes" %} change the version number if anything changes in the library.

{% include requirement/MUST id="python-versioning-patch" %} increment the patch version if only bug fixes are added to the package.

{% include requirement/MUST id="python-verioning-minor" %} increment the minor version if any new functionality is added to the package.

{% include requirement/MUST id="python-versioning-apiversion" %} increment the minor version if the default REST API version is changed, even if there's no public API change to the library.

{% include requirement/MUSTNOT id="python-versioning-api-major" %} increment the major version for a new REST API version unless it requires breaking API changes in the python library itself.

{% include requirement/MUST id="python-versioning-major" %} increment the major version if there are breaking changes in the package. Breaking changes require prior approval from the [Architecture Board].

The bar to make a breaking change is extremely high for GA client libraries. We may create a new package with a different name to avoid diamond dependency issues.

REST API method versioning

{% include requirement/MUST id="python-versioning-latest-service-api" %} use the latest service protocol version when making requests.

{% include requirement/MUST id="python-versioning-select-service-api" %} allow a client application to specify an earlier version of the service protocol.

Packaging

{% include requirement/MUST id="python-packaging-name" %} name your package after the namespace of your main client class.

{% include requirement/MUST id="python-packaging-name-allowed-chars" %} use all lowercase in your package name with a dash (-) as a separator.

{% include requirement/MUSTNOT id="python-packaging-name-disallowed-chars" %} use underscore (_) or period (.) in your package name. If your namespace includes underscores, replace them with dash (-) in the distribution package name.

{% include requirement/MUST id="python-packaging-follow-repo-rules" %} follow the specific package guidance from the azure-sdk-packaging wiki

{% include requirement/MUST id="python-packaging-follow-python-rules" %} follow the namespace package recommendations for Python 3.x for packages that only need to target 3.x.

{% include requirement/MUST id="python-packaging-nspkg" %} depend on azure-nspkg for Python 2.x.

{% include requirement/MUST id="python-packaging-init" %} include __init__.py for the namespace(s) in sdists

Binary extensions

{% include requirement/MUST id="python-native-approval" %} be approved by the [Architecture Board].

{% include requirement/MUST id="python-native-plat-support" %} support Windows, Linux (manylinux - see PEP513, PEP571), and MacOS. Support the earliest possible manylinux to maximize your reach.

{% include requirement/MUST id="python-native-arch-support" %} support both x86 and x64 architectures.

{% include requirement/MUST id="python-native-charset-support" %} support unicode and ASCII versions of CPython 2.7.

Testing

{% include requirement/MUST id="python-testing-pytest" %} use pytest as the test framework.

{% include requirement/SHOULD id="python-testing-async" %} use pytest-asyncio for testing of async code.

{% include requirement/MUST id="python-testing-live" %} make your scenario tests runnable against live services. Strongly consider using the Python Azure-DevTools package for scenario tests.

{% include requirement/MUST id="python-testing-record" %} provide recordings to allow running tests offline/without an Azure subscription

{% include requirement/MUST id="python-testing-parallel" %} support simultaneous test runs in the same subscription.

{% include requirement/MUST id="python-testing-independent" %} make each test case independent of other tests.

{% include requirement/MUST id="python-tooling-pylint" %} use pylint for your code. Use the pylintrc file in the root of the repository.

{% include requirement/MUST id="python-tooling-flake8" %} use flake8-docstrings to verify doc comments.

{% include requirement/MUST id="python-tooling-black" %} use Black for formatting your code.

{% include requirement/SHOULD id="python-tooling-mypy" %} use MyPy to statically check the public surface area of your library.

You don't need to check non-shipping code such as tests.

{% include refs.md %} {% include_relative refs.md %}