diff --git a/README.md b/README.md index 4b8bc7fa897..ae65b463971 100755 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additio [Design Guidelines for iOS]: https://azure.github.io/azure-sdk/ios_introduction.html [Design Guidelines for Java]: https://azure.github.io/azure-sdk/java_introduction.html [Design Guidelines for TypeScript]: https://azure.github.io/azure-sdk/typescript_introduction.html -[Design Guidelines for Python]: https://azure.github.io/azure-sdk/python_introduction.html +[Design Guidelines for Python]: https://azure.github.io/azure-sdk/python_design.html [revproc]: https://azure.github.io/azure-sdk/policies_reviewprocess.html [azure-sdk Repository]: https://github.com/Azure/azure-sdk diff --git a/_data/sidebars/general_sidebar.yml b/_data/sidebars/general_sidebar.yml index 07e7d1bc448..37f8cc16c39 100644 --- a/_data/sidebars/general_sidebar.yml +++ b/_data/sidebars/general_sidebar.yml @@ -71,9 +71,7 @@ entries: external_url: https://azure.github.io/azure-sdk-for-java/storage.html#azure-storage-queue - title: Python Guidelines folderitems: - - title: Introduction - url: /python_introduction.html - - title: API Design + - title: Design url: /python_design.html - title: Implementation url: /python_implementation.html diff --git a/_data/topnav.yml b/_data/topnav.yml index 4e600e281d1..99aedd84f9d 100644 --- a/_data/topnav.yml +++ b/_data/topnav.yml @@ -29,7 +29,7 @@ topnav_dropdowns: - title: Java url: /java_introduction.html - title: Python - url: /python_introduction.html + url: /python_design.html - title: TypeScript url: /typescript_introduction.html - title: API References diff --git a/_includes/refs.md b/_includes/refs.md index 122e535d1e1..6ca1e022aac 100644 --- a/_includes/refs.md +++ b/_includes/refs.md @@ -14,7 +14,7 @@ [golang-guidelines]: {{ site.baseurl }}{% link docs/golang/introduction.md %} [ios-guidelines]: {{ site.baseurl }}{% link docs/ios/introduction.md %} [java-guidelines]: {{ site.baseurl }}{% link docs/java/introduction.md %} -[python-guidelines]: {{ site.baseurl }}{% link docs/python/introduction.md %} +[python-guidelines]: {{ site.baseurl }}{% link docs/python/design.md %} [typescript-guidelines]: {{ site.baseurl }}{% link docs/typescript/introduction.md %} [android-latest-releases]: {{ site.baseurl }}{% link releases/latest/android.md %} diff --git a/docs/python/_includes/example_client.py b/docs/python/_includes/example_client.py new file mode 100644 index 00000000000..e066216c54c --- /dev/null +++ b/docs/python/_includes/example_client.py @@ -0,0 +1,122 @@ +"""Example client using some of the most common API patterns +""" + +import models +import azure.core.pipeline.transport as transports + +class Thing(object): + """A simple model type representing a Thing. + + :ivar name: The name of the thing. + :vartype name: str + :ivar size: The size of the thing. + :vartype size: int + """ + + def __init__(self, name, size): + # type: (str, number) -> None + """Create a new Thing + + :param name: The name of the thing + :type name: str + :param size: The size of the thing + :type size: int + """ + + # Please note that we are using attributes rather than properties. + self.name = name + self.size = size + + @classmethod + def from_response(self, response): + # type: (azure.core.pipeline.HttpResponse) -> Thing + """Factory method to, given a response, construct a ~Thing + """ + return Thing(**response.context['deserialized_data']) + + def __repr__(self): + # For simple model types, we can just dump our __dict__ and + # truncate the output at 1024 characters. + return json.dumps(self.__dict__)[:1024] + +class ExampleClient(object): + + def __init__(self, endpoint, credential, **kwargs): + # type: (str, azure.core.credential.TokenCredential, **Any) -> None + """Create a new example client instance + + :param endpoint: Endpoint to connect to. + :type endpoint str: + :param credential: Credentials to use when connecting to the service. + :type credential: ~azure.core.credentials.TokenCredential + :keyword apiversion: API version to use when talking to the service. Default is '2020-12-31' + :type apiversion: str + :keyword transport: HttpTransport to use. Default is ~transports.RequestsHttpTransport. + :type transport: ~transports.HttpTransport + """ + self._api_version = kwargs.pop('api_version', '2020-12-31') + transport = kwargs.pop('transport', None) or transports.RequestsTransport(**kwargs) + + # continue to build up your client... + self._pipeline = [ + ..., # List of policies for this specific client + transport + ] + + @classmethod + def from_connection_string(cls, connection_string, **kwargs): + # type: (str, **Any) -> None + """Optional factory method if the service supports connection strings + + :param connection_string: Connection string containing endpoint and credentials + :type connection_string: str + :returns: The newly created client. + :rtype: ~ExampleClient + """ + endpoint, credential = _parse(connection_string) + return cls(endpoint, credential, **kwargs) + + + def get_thing(self, name, **kwargs): + # type: (str, **Any) -> Thing + """Get the Thing with name `name`. + + :param name: The name of the ~Thing to get + :type name: str + :rtype: ~Thing + """ + model_factory = kwargs.pop('cls', Thing.from_response) + request = self._build_get_thing_request(name) + # Pass along all policy parameters when making the request + response = self._pipeline.send(request, **kwargs) + return model_factory(response) + + def list_things(self, **kwargs): + # type: (**Any) -> azure.core.ItemPaged[Thing] + """List all things. + + :rtype: ~azure.core.ItemPaged[~Thing] + """ + ... + return azure.core.paging.ItemPaged(...) + + def begin_restart_thing(self, name, **kwargs) -> azure.core.polling.LROPoller[bool]: + # type: (str, **Any) -> azure.core.polling.LROPoller[bool] + """Restart the thing + + :param name: The name of the thing to restart + :type name: str + """ + model = kwargs.pop('cls', dict) + request = self._build_begin_restart_thing(name) + # Pass along all policy parameters when making the request + response = self._pipeline.send(request, **kwargs) + + # TODO: show how to construct the poller instance + return azure.core.polling.LROPoller(...) + + + + + + diff --git a/docs/python/design.md b/docs/python/design.md index f93773b6307..f92aabc790c 100644 --- a/docs/python/design.md +++ b/docs/python/design.md @@ -8,62 +8,82 @@ sidebar: general_sidebar ## Introduction -### Support for non-HTTP protocols +### Design principles -TODO: In Introduction section, call out that API designers for non-HTTP based services to reach out to the arch board for guidance. +The Azure SDK should be designed to enhance the productivity of developers connecting to Azure services. Other qualities (such as completeness, extensibility, and performance) are important but secondary. Productivity is achieved by adhering to the principles described below: -The API surface of your client library must have the most thought as it is the primary interaction that the consumer has with your service. +#### Idiomatic -{% include requirement/MUST id="python-feature-support" %} support 100% of the features provided by the Azure service the client library represents. Gaps in functionality cause confusion and frustration among developers. +* The SDK should follow the design guidelines and conventions for the target language. It should feel natural to a developer in the target language. +* We embrace the ecosystem with its strengths and its flaws. +* We work with the ecosystem to improve it for all developers. -## Namespaces +#### Consistent -{% include requirement/MUST id="python-namespaces-prefix" %} implement your library as a subpackage in the `azure` namespace. +* Client libraries should be consistent within the language, consistent with the service and consistent between all target languages. In cases of conflict, consistency within the language is the highest priority and consistency between all target languages is the lowest priority. +* Service-agnostic concepts such as logging, HTTP communication, and error handling should be consistent. The developer should not have to relearn service-agnostic concepts as they move between client libraries. +* Consistency of terminology between the client library and the service is a good thing that aids in diagnosability. +* All differences between the service and client library must have a good (articulated) reason for existing, rooted in idiomatic usage rather than whim. +* The Azure SDK for each target language feels like a single product developed by a single team. +* There should be feature parity across target languages. This is more important than feature parity with the service. -{% include requirement/MUST id="python-namespaces-naming" %} pick a package name that allows the consumer to tie the namespace to the service being used. As a default, use the compressed service name at the end of the namespace. The namespace does NOT change when the branding of the product changes. Avoid the use of marketing names that may change. +#### Approachable -A compressed service name is the service name without spaces. It may further be shortened if the shortened version is well known in the community. For example, “Azure Media Analytics” would have a compressed service name of `mediaanalytics`, and “Azure Service Bus” would become `servicebus`. Separate words using an underscore if necessary. If used, `mediaanalytics` would become `media_analytics` +* We are experts in the supported technologies so our customers, the developers, don't have to be. +* Developers should find great documentation (hero tutorial, how to articles, samples, and API documentation) that makes it easy to be successful with the Azure service. +* Getting off the ground should be easy through the use of predictable defaults that implement best practices. Think about progressive concept disclosure. +* The SDK should be easily acquired through the most normal mechanisms in the target language and ecosystem. +* Developers can be overwhelmed when learning new service concepts. The core use cases should be discoverable. -{% include requirement/MAY id="python-namespaces-grouping" %} include a group name segment in your namespace (for example, `azure..`) if your service or family of services have common behavior (for example, shared authentication types). +#### Diagnosable -If you want to use a group name segment, use one of the following groups: +* The developer should be able to understand what is going on. +* It should be discoverable when and under what circumstances a network call is made. +* Defaults are discoverable and their intent is clear. +* Logging, tracing, and exception handling are fundamental and should be thoughtful. +* Error messages should be concise, correlated with the service, actionable, and human readable. Ideally, the error message should lead the consumer to a useful action that they can take. +* Integrating with the preferred debugger for the target language should be easy. -{% include tables/data_namespaces.md %} +#### Dependable -{% include requirement/MUST id="python-namespaces-mgmt" %} place management (Azure Resource Manager) APIs in the `mgmt` group. Use the grouping `azure.mgmt.` for the namespace. Since more services require control plane APIs than data plane APIs, other namespaces may be used explicitly for control plane only. +* Breaking changes are more harmful to a user's experience than most new features and improvements are beneficial. +* Incompatibilities should never be introduced deliberately without thorough review and very strong justification. +* Do not rely on dependencies that can force our hand on compatibility. -{% include requirement/MUST id="python-namespaces-register" %} register the chosen namespace with the [Architecture Board]. Open an issue to request the namespace. See [the registered namespace list](registered_namespaces.html) for a list of the currently registered namespaces. +### General guidelines -{% include requirement/MUST id="python-namespaces-async" %} use an `.aio` suffix added to the namespace of the sync client for async clients. +The API surface of your client library must have the most thought as it is the primary interaction that the consumer has with your service. -Example: +{% include requirement/MUST id="python-feature-support" %} support 100% of the features provided by the Azure service the client library represents. Gaps in functionality cause confusion and frustration among developers. -```python -# Yes: -from azure.exampleservice.aio import ExampleServiceClient +### Non-HTTP based services -# No: Wrong namespace, wrong client name... -from azure.exampleservice import AsyncExampleServiceClient -``` +These guidelines were written primarily with a HTTP based request/response in mind, but many general guidelines apply to other types of services as well. This includes, but is not limited to, packaging and naming, tools and project structures. -## Clients +Please contact the [Azure SDK Architecture Board] for more guidance on non HTTP/REST based services. -TODO: Add an example of the basic API shape similar to what's shown in the code sample but with the equivalent in Python. Please include code illustrations of the following areas: +### Supported python versions -- Service Client -- Client Construction -- Service Methods -- Model Types -- Common Namespace -- Common Auth +{% include requirement/MUST id="python-general-version-support" %} support Python 2.7 and 3.5.3+. -See Azure SDK Core Shape slides shown at **github.com/Azure/azure-sdk-pr/issues/440** for details. +{% include requirement/SHOULD id="python-general-universal-pkg" %} provide a [universal package] that works on all supported versions of Python, unless there's a compelling reason to have separate Python2 and Python3 packages. +For example, if you depend on different external packages for Python2 and Python3, and neither external dependency is available for both Python versions. + +## Azure SDK API Design Your API surface will consist of one or more _service clients_ that the consumer will instantiate to connect to your service, plus a set of supporting types. +### Service client + +The service client is the primary entry point for users of the library. A service client exposes one or more methods that allow them to interact with the service. + +{% include requirement/MUST id="python-client-namespace" %} expose the service clients the user is more likely to interact with from the root namespace of your package. Specialized service clients may be placed in sub-namespaces. + {% include requirement/MUST id="python-client-naming" %} name service client types with a **Client** suffix. +{% include requirement/MUST id="python-client-sync-async-separate-clients" %} provide separate sync and async clients. See the [Async Support](#async-support) section for more information. + ```python # Yes class CosmosClient(object) ... @@ -73,31 +93,529 @@ class CosmosProxy(object) ... # No class CosmosUrl(object) ... +``` + +{% include requirement/MUST id="python-client-immutable" %} make the service client immutable. See the [Client Immutability](#client-immutability) section for more information. + +#### Constructors and factory methods + +Only the minimal information needed to connect and interact with the service should be required in order to construct a client instance. All additional information should be optional and passed in as optional keyword-only arguments. + +##### Client configuration + +{% include requirement/MUST id="python-client-constructor-form" %} provide a constructor that takes positional binding parameters (for example, the name of, or a URL pointing to the service instance), a positional `credential` parameter, a `transport` keyword-only parameter, and keyword-only arguments (emulated using `**kwargs` for Python 2.7 support) for passing settings through to individual HTTP pipeline policies. See the [Authentication](#authentication) section for more information on the `credential` parameter. + +{% include requirement/MUSTNOT id="python-client-options-naming" %} use an "options bag" object to group optional parameters. Instead, pass as individual keyword-only arguments. + +{% include requirement/MUST id="python-client-constructor-policy-arguments" %} accept optional default request options as keyword arguments and pass them along to its pipeline policies. See [Common service operation parameters](#common-service-operation-parameters) for more information. + +```python +# Change default number of retries to 18 and overall timeout to 2s. +client = ExampleClient('https://contoso.com/xmpl', + DefaultAzureCredential(), + max_retries=18, + timeout=2) +``` + +{% include requirement/MUST id="python-client-constructor-transport-argument" %} allow users to pass in a `transport` keyword-only argument that allows the caller to specify a specific transport instance. The default value should be the [`RequestsTransport`](https://azuresdkdocs.blob.core.windows.net/$web/python/azure-core/1.1.1/azure.core.pipeline.transport.html?highlight=transport#azure.core.pipeline.transport.RequestsTransport) for synchronous clients and the [`AioHttpTransport`](https://azuresdkdocs.blob.core.windows.net/$web/python/azure-core/1.1.1/azure.core.pipeline.transport.html?highlight=transport#azure.core.pipeline.transport.AioHttpTransport) for async clients. + +{% include requirement/MUST id="python-client-connection-string" %} use a separate factory classmethod `from_connection_string` to create a client from a connection string (if the client supports connection strings). The `from_connection_string` factory method should take the same set of arguments (excluding information provided in the connection string) as the constructor. The constructor (`__init__` method) **must not** take a connection string, even if it means that using the `from_connection_string` is the only supported method to create an instance of the client. + +The method **should** parse the connection string and pass the values along with any additional keyword-only arguments except `credential` to the constructor. Only provide a `from_connection_string` factory method if the Azure portal exposes a connection string for your service. + +```python +class ExampleClientWithConnectionString(object): + + @classmethod + def _parse_connection_string(cls, connection_string): ... + + @classmethod + def from_connection_string(cls, connection_string, **kwargs): + endpoint, credential = cls._parse_connection_string(connection_string) + return cls(endpoint, credential, **kwargs) +``` + +```python +{% include_relative _includes/example_client.py %} +``` + +{% include requirement/MAY id="python-client-constructor-from-url" %} use a separate factory classmethod `from__url` (e.g. `from_blob_url`) to create a client from a URL (if the service relies on passing URLs to resources around - e.g. Azure Blob Storage). The `from_url` factory method should take the same set of optional keyword arguments as the constructor. + +##### Specifying the Service Version + +{% include requirement/MUST id="python-client-constructor-api-version-argument-1" %} accept an optional `api_version` keyword-only argument of type string. If specified, the provided api version MUST be used when interacting with the service. If the parameter is not provided, the default value MUST be the latest non-preview API version understood by the client library (if there the service has a non-preview version) or the latest preview API version understood by the client library (if the service does not have any non-preview API versions yet). This parameter MUST be available even if there is only one API version understood by the service in order to allow library developers to lock down the API version they expect to interact with the service with. + +```python +from azure.identity import DefaultAzureCredential + +# By default, use the latest supported API version +latest_known_version_client = ExampleClient('https://contoso.com/xmpl', + DefaultAzureCredential()) + +# ...but allow the caller to specify a specific API version as welll +specific_api_version_client = ExampleClient('https://contoso.com/xmpl', + DefaultAzureCredential(), + api_version='1971-11-01') +``` + +{% include requirement/MUST id="python-client-constructor-api-version-argument-2" %} document the service API version that is used by default. + +{% include requirement/MUST id="python-client-constructor-api-version-argument-3" %} document in which API version a feature (function or parameter) was introduced in if not all service API versions support it. + +{% include requirement/MAY id="python-client-constructor-api-version-argument-4" %} validate the input `api_version` value against a list of supported API versions. + +{% include requirement/MAY id="python-client-constructor-api-version-argument-5" %} include all service API versions that are supported by the client library in a `ServiceVersion` enumerated value. + +##### Additional constructor parameters + +|Name|Description| +|-|-| +|`credential`|Credentials to use when making service requests (See [Authentication](#authentication))| +|`application_id`|Name of the client application making the request. Used for telemetry| +|`api_version`|API version to use when making service requests (See [Service Version](#specifying-the-service-version)) | +|`transport`|Override the default HTTP transport (See [Client Configuration](#client-configuration))| + +##### Client immutability + +{% include requirement/MUST id="python-client-immutable-design" %} design the client to be immutable. This does not mean that you need to use read-only properties (attributes are still acceptable), but rather that the there should not be any scenarios that require callers to change properties/attributes of the client. + +#### Service methods + +##### Naming + +{% include requirement/SHOULD id="python-client-service-verbs" %} prefer the usage one of the preferred verbs for method names. You should have a good (articulated) reason to have an alternate verb for one of these operations. +|Verb|Parameters|Returns|Comments| +|-|-|-|-| +|`create_\`|key, item, `[allow_overwrite=False]`|Created item|Create new item. Fails if item already exists.| +|`upsert_\`|key, item|item|Create new item, or update existing item. Verb is primarily used in database-like services | +|`set_\`|key, item|item|Create new item, or update existing item. Verb is primarily used for dictionary-like properties of a service | +|`update_\`|key, partial item|item|Fails if item doesn't exist. | +|`replace_\`|key, item|item|Completely replaces an existing item. Fails if the item doesn't exist. | +|`append_\`|item|item|Add item to a collection. Item will be added last. | +|`add_\`|index, item|item|Add item to a collection. Item will be added at the given index. | +|`get_\`|key|item|Raises an exception if item doesn't exist | +|`list_\`||`azure.core.ItemPaged[Item]`|Return an iterable of `Item`s. Returns an iterable with no items if no items exist (doesn't return `None` or throw)| +|`\\_exists`|key|`bool`|Return `True` if the item exists. Must raise an exception if the method failed to determine if the item exists (for example, the service returned an HTTP 503 response)| +|`delete_\`|key|`None`|Delete an existing item. Must succeed even if item didn't exist.| +|`remove_\`|key|removed item or `None`|Remove a reference to an item from a collection. This method doesn't delete the actual item, only the reference.| + +{% include requirement/MUST id="python-client-standardize-verbs" %} standardize verb prefixes outside the list of preferred verbs for a given service across language SDKs. If a verb is called `download` in one language, we should avoid naming it `fetch` in another. + +{% include requirement/MUST id="python-lro-prefix" %} prefix methods with `begin_` for [long running operations](#methods-invoking-long-running-operations). + +{% include requirement/MUST id="python-paged-prefix" %} prefix methods with `list_` for methods that enumerate (lists) resources + +##### Return types + +Requests to the service fall into two basic groups - methods that make a single logical request, or a deterministic sequence of requests. An example of a single logical request is a request that may be retried inside the operation. An example of a deterministic sequence of requests is a paged operation. + +The logical entity is a protocol neutral representation of a response. For HTTP, the logical entity may combine data from headers, body, and the status line. For example, you may wish to expose an `ETag` header as an `etag` attribute on the logical entity. For more information see [Model Types](#model-types). + +{% include requirement/MUST id="python-response-logical-entity" %} optimize for returning the logical entity for a given request. The logical entity MUST represent the information needed in the 99%+ case. + +{% include requirement/MUST id="python-response-exception-on-failure" %} raise an exception if the method call failed to accomplish the user specified task. This includes both situations where the service actively responded with a failure as well as when no response was received. See [Exceptions](#exceptions) for more information. + +```python +client = ComputeClient(...) + +try: + # Please note that there is no status code etc. as part of the response. + # If the call fails, you will get an exception that will include the status code + # (if the request was made) + virtual_machine = client.get_virtual_machine('example') + print(f'Virtual machine instance looks like this: {virtual_machine}') +except azure.core.exceptions.ServiceRequestError as e: + print(f'Failed to make the request - feel free to retry. But the specifics are here: {e}') +except azure.core.exceptions.ServiceResponseError as e: + print(f'The request was made, but the service responded with an error. Status code: {e.status_code}') ``` -{% include requirement/MUST id="python-client-namespace" %} expose the service clients the user is more likely to interact with from the root namespace of your package. +Do not return `None` or a `boolean` to indicate errors: + +```python +# Yes +try: + resource = client.create_resource(name) +except azure.core.errors.ResourceExistsException: + print('Failed - we need to fix this!') -### Constructors and factory methods +# No +resource = client.create_resource(name): +if not resource: + print('Failed - we need to fix this!') +``` -{% include requirement/MUST id="python-client-constructor-form" %} provide a constructor that takes binding parameters (for example, the name of, or a URL pointing to the service instance), a `credentials` parameter, a `transport` parameter, and `**kwargs` for passing settings through to individual HTTP pipeline policies. +{% include requirement/MUSTNOT id="python-errors-normal-responses" %} throw an exception for "normal responses". -Only the minimal information needed to connect and interact with the service should be required. All additional information should be optional. +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: -The constructor **must not** take a connection string. +```python +# 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/MUST id="python-client-connection-string" %} use a separate factory method `ExampleServiceClient.from_connection_string` to create a client from a connection string (if the client supports connection strings). +##### Cancellation -The method **should** parse the connection string and pass the values to the constructor. Provide a `from_connection_string` factory method only if the Azure portal exposes a connection string for your service. +{% include requirement/MUST id="python-client-cancellation-sync-methods" %} provide an optional keyword argument `timeout` to allow callers to specify how long they are willing to wait for the method to complete. The `timeout` is in seconds, and should be honored to the best extent possible. -TODO: Please include a code sample here. + {% include requirement/MUST id="python-client-cancellation-async-methods" %} use the standard [asyncio.Task.cancel](https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel) method to cancel async methods. -TODO: Please include a code sample for setting the service version. +#### Service Method Parameters +{% include requirement/MUST id="python-client-optional-arguments-keyword-only" %} provide optional operation-specific arguments as keyword only. See [positional and keyword-only arguments] for more information. -TODO: If client configuration options are relevant to Python (e.g. does an API designer need to do something special to enable configuration of retries? Or other client-specific options like we designed for Text Analytics?), please add a subsection on designing these here. +{% include requirement/MUST id="python-client-service-per-call-args" %} provide keyword-only arguments that override per-request policy options. The name of the parameters MUST mirror the name of the arguments provided in the client constructor or factory methods. +For a full list of supported optional arguments used for pipeline policy and transport configuration (both at the client constructor and per service operation), see the [Azure Core developer documentation](https://github.com/Azure/azure-sdk-for-python/blob/master/sdk/core/azure-core/CLIENT_LIBRARY_DEVELOPER.md). -TODO: Are clients immutable in Python as they are in other languages? If so, please call that out in this or above Service Client section. +{% include requirement/MUST id="python-client-service-args-conflict" %} qualify a service parameter name if it conflicts with any of the documented pipeline policy or transport configuration options used with all service operations and client constructors. + +```python +# Set the default number of retries to 18 and timeout to 2s for this client instance. +client = ExampleClient('https://contoso.com/xmpl', DefaultAzureCredential(), max_retries=18, timeout=2) + +# Override the client default timeout for this specific call to 32s (but max_retries is kept to 18) +client.do_stuff(timeout=32) +``` + +##### 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. + +```python +# No: +def get_thing(name: "str") -> "str": + url = f'https:///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:///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" %} verify that 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. + +##### Common service operation parameters + +{% include requirement/MUST id="python-client-service-args" %} support the common arguments for service operations: + +|Name|Description|Applies to|Notes| +|-|-|-|-| +|`timeout`|Timeout in seconds|All service methods| +|`headers`|Custom headers to include in the service request|All requests|Headers are added to all requests made (directly or indirectly) by the method.| +|`client_request_id`|Caller specified identification of the request.|Service operations for services that allow the client to send a client-generated correlation ID.|Examples of this include `x-ms-client-request-id` headers.|The client library **must** use this value if provided, or generate a unique value for each request when not specified.| +|`response_hook`|`callable` that is called with (response, headers) for each operation.|All service methods| + +{% include requirement/MUST id="python-client-splat-args" %} accept a Mapping (`dict`-like) object in the same shape as a serialized model object for parameters. + +```python +# Yes: +class Thing(object): + + def __init__(self, name, size): + self.name = name + self.size = size + +def do_something(thing: "Thing"): + ... + +do_something(Thing(name='a', size=17)) # Works +do_something({'name': 'a', 'size', '17'}) # Does the same thing... +``` + +{% include requirement/MUST id="python-client-flatten-args" %} use "flattened" named arguments for `update_` methods. **May** additionally take the whole model instance as a named parameter. If the caller passes both a model instance and individual key=value parameters, the explicit key=value parameters override whatever was specified in the model instance. + +```python +class Thing(object): + + def __init__(self, name, size, description): + self.name = name + self.size = size + self.description = description + + def __repr__(self): + return json.dumps({ + "name": self.name, "size": self.size, "description": self.description + })[:1024] + +class Client(object): + + def update_thing(self, name=None, size=None, thing=None): ... + +thing = Thing(name='hello', size=4711, description='This is a description...') + +client.update_thing(thing=thing, size=4712) # Will send a request to the service to update the model's size to 4712 +thing.description = 'Updated' +thing.size = -1 +# Will send a request to the service to update the model's size to 4713 and description to 'Updated' +client.update_thing(name='hello', size=4713, thing=thing) +``` + +#### Methods returning collections (paging) + +Services may require multiple requests to retrieve the complete set of items in large collections. This is generally done by the service returning a partial result, and in the response providing a token or link that the client can use to retrieve the next batch of responses in addition to the set of items. + +In Azure SDK for Python cilent libraries, this is exposed to users through the [ItemPaged protocol](#python-core-protocol-paged). The `ItemPaged` protocol optimizes for retrieving the full set of items rather than forcing users to deal with the underlying paging. + +{% include requirement/MUST id="python-response-paged-protocol" %} return a value that implements the [ItemPaged protocol](#python-core-protocol-paged) for operations that return collections. The [ItemPaged protocol](#python-core-protocol-paged) allows the user to iterate through all items in a returned collection, and also provides a method that gives access to individual pages. + +```python +client = ExampleClient(...) + +# List all things - paging happens transparently in the +# background. +for thing in client.list_things(): + print(thing) + +# The protocol also allows you to list things by page... +for page_no, page in enumerate(client.list_things().by_page()): + print(page_no, page) +``` + +{% include requirement/MAY id="python-response-paged-results" %} expose a `results_per_page` keyword-only parameter where supported by the service (e.g. an OData `$top` query parameter). + +{% include requirement/SHOULDNOT id="python-response-paged-continuation" %} expose a continuation parameter in the `list_` client method - this is supported in the `by_page()` function. + +```python +client = ExampleClient(...) + +# No - don't pass in the continuation token directly to the method... +for thing in client.list_things(continuation_token='...'): + print(thing) + +# Yes - provide a continuation_token to in the `by_page` method... +for page in client.list_things().by_page(continuation_token='...'): + print(page) +``` + +{% include requirement/MUST id="python-paged-non-server-paged-list" %} return a value that implements the [ItemPaged protocol](#python-core-protocol-paged) even if the service API currently do not support server driven paging. This allows server driven paging to be added to the service API without introducing breaking changes in the client library. + +#### Methods invoking long running operations + +Service operations that take a long time (currently defined in the [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#141-principles) as not completing in 0.5s in P99) to complete are modeled by services as long running operations. + +Python client libraries abstracts the long running operation using the [Long running operation Poller protocol](#python-core-protocol-lro-poller). +In cases where a service API is not explicitly implemented as a long-running operation, but the common usage pattern requires a customer to sleep or poll a status - it's likely that these API's should still be represented in the SDK using the Poller protocol. + +{% include requirement/MUST id="python-lro-poller" %} return an object that implements the [Poller protocol](#python-core-protocol-lro-poller) for long running operations. + +{% include requirement/MUST id="python-lro-poller-begin-naming" %} use a `begin_` prefix for all long running operations. + +#### Conditional request methods + +{% include requirement/MUST id="python-method-conditional-request" %} add a keyword-only `match_condition` parameter for service methods that support conditional requests. The parameter should support the `azure.core.MatchConditions` type defined in `azure-core` as input. + +{% include requirement/MUST id="python-method-conditional-request-etag" %} add a keyword-only `etag` parameter for service methods that support conditional requests. For service methods that take a model instance that has an `etag` property, the explicit `etag` value passed in overrides the value in the model instance. + +```python +class Thing(object): + + def __init__(self, name, etag): + self.name = name + self.etag = etag + +thing = client.get_thing('theName') + +# Uses the etag from the retrieved thing instance.... +client.update_thing(thing, name='updatedName', match_condition=azure.core.MatchConditions.IfNotModified) + +# Uses the explicitly provided etag. +client.update_thing(thing, name='updatedName2', match_condition=azure.core.MatchConditions.IfNotModified, etag='"igotthisetagfromsomewhereelse"') +``` + +#### Hierarchical clients + +Many services have resources with nested child (or sub) resources. For example, Azure Storage provides an account that contains zero or more containers, which in turn contains zero or more blobs. + +{% include requirement/MUST id="python-client-hierarchy" %} create a client type corresponding to each level in the hierarchy except for leaf resource types. You **may** omit creating a client type for leaf node resources. + +{% include requirement/MUST id="python-client-hier-creation" %} make it possible to directly create clients for each level in the hierarchy. The constructor can be called directly or via the parent. + +```python +class ChildClient: + # Yes: + __init__(self, parent, name, credentials, **kwargs) ... + +class ChildClient: + # Yes: + __init__(self, url, credentials, **kwargs) ... +``` + +{% include requirement/MUST id="python-client-hier-vend" %} provide a `get__client(self, name, **kwargs)` method to retrieve a client for the named child. The method must not make a network call to verify the existence of the child. + +{% include requirement/MUST id="python-client-hier-create" %} provide method `create_(...)` that creates a child resource. The method **should** return a client for the newly created child resource. + +{% include requirement/SHOULD id="python-client-hier-delete" %} provide method `delete_(...)` that deletes a child resource. + +### Supporting types + +#### Model types + +Client libraries represent entities transferred to and from Azure services as model types. Certain types are used for round-trips to the service. They can be sent to the service (as an addition or update operation) and retrieved from the service (as a get operation). These should be named according to the type. For example, a `ConfigurationSetting` in App Configuration, or a `VirtualMachine` on for Azure Resource Manager. + +Data within the model type can generally be split into two parts - data used to support one of the champion scenarios for the service, and less important data. Given a type `Foo`, the less important details can be gathered in a type called `FooDetails` and attached to `Foo` as the `details` attribute. + +{% include requirement/MUST id="python-models-input-dict" %} support dicts as alternative inputs to model types. + +{% include requirement/MUST id="python-models-input-constructor" %} craft a constructor for models that are intended to be instantiated by a user (i.e. non-result types) with minimal required information and optional information as keyword-only arguments. + +{% include requirement/MAY id="python-models-generated" %} expose models from the generated layer by adding to the root `__init__.py` (and `__all__`) if they otherwise meet the guidelines. + +{% include requirement/MUSTNOT id="python-models-async" %} duplicate models between the root and `aio` namespace. This means models should not use any syntax incompatible with Python 2.7 (e.g. type hint syntax). + +In order to facilitate round-trip of responses (common in get resource -> conditionally modify resource -> set resource workflows), output model types should use the input model type (e.g. `ConfigurationSetting`) whenever possible. The `ConfigurationSetting` type should include both server generated (read-only) attributes even though they will be ignored when used as input to the set resource method. + +- `Item` for each item in an enumeration if the enumeration returns a partial schema for the model. For example, GetBlobs() return an enumeration of BlobItem, which contains the blob name and metadata, but not the content of the blob. +- `Result` for the result of an operation. The `` is tied to a specific service operation. If the same result can be used for multiple operations, use a suitable noun-verb phrase instead. For example, use `UploadBlobResult` for the result from `UploadBlob`, but `ContainerChangeResult` for results from the various methods that change a blob container. + +{% include requirement/MUST id="python-models-dict-result" %} use a simple Mapping (e.g. `dict`) rather than creating a `Result` class if the `Result` class is not used as an input parameter for other APIs. + +The following table enumerates the various models you might create: + +|Type|Example|Usage| +|-|-| +||Secret|The full data for a resource| +|Details|SecretDetails|Less important details about a resource. Attached to .details| +|Item|SecretItem|A partial set of data returned for enumeration| +|Result|AddSecretResult|A partial or different set of data for a single operation| +|Result|SecretChangeResult|A partial or different set of data for multiple operations on a model| + +```python +# An example of a model type. +class ConfigurationSetting(object): + """Model type representing a configuration setting + + :ivar name: The name of the setting + :vartype name: str + :ivar value: The value of the setting + :vartype value: object + """ + + def __init__(self, name, value): + # type: (str, object) -> None + self.name = name + self.value = value + + def __repr__(self): + # type: () -> str + return json.dumps(self.__dict__)[:1024] +``` + +#### Enumerations + +{% include requirement/MUST id="python-models-enum-string" %} use extensible enumerations. + +{% include requirement/MUST id="python-models-enum-name-uppercase" %} use UPPERCASE names for enum names. + +```python + +# Yes +class MyGoodEnum(str, Enum): + ONE = 'one' + TWO = 'two' + +# No +class MyBadEnum(str, Enum): + One = 'one' # No - using PascalCased name. + two = 'two' # No - using all lower case name. +``` + +### Exceptions + +{% include requirement/SHOULD id="python-errors-azure-exceptions" %} prefer raising [existing exception types from the `azure-core`](https://azuresdkdocs.blob.core.windows.net/$web/python/azure-core/1.9.0/index.html#azure-core-library-exceptions) package over creating new exception types. + +{% include requirement/MUSTNOT id="python-errors-use-standard-exceptions" %} create new exception types when a [built-in exception type](https://docs.python.org/3/library/exceptions.html) will suffice. + +{% include requirement/SHOULDNOT id="python-errors-new-exceptions" %} create a new exception type unless the developer can handle the error programmatically. Specialized exception types related to service operation failures should be based on existing exception types from the [`azure-core`](https://azuresdkdocs.blob.core.windows.net/$web/python/azure-core/1.9.0/index.html#azure-core-library-exceptions) package. + +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 (e.g. `ValueError`, `TypeError`, `RuntimeError` etc.) + +### Authentication + +{% include requirement/MUST id="python-auth-credential-azure-core" %} use the credentials classes in `azure-core` whenever possible. + +{% include requirement/MUST id="python-auth-policy-azure-core" %} use authentication policy implementations in `azure-core` whenever possible. + +{% include requirement/MAY id="python-auth-service-credentials" %} add additional credential types if required by the service. Contact the [Azure SDK Architecture Board] for guidance if you believe you have need to do so. + +{% include requirement/MUST id="python-auth-service-support" %} support all authentication methods that the service supports. + +### Namespaces + +In the guidelines below, the term "namespace" is used to denote a python package or module (i.e. something that you would `import` in your code). The term "distribution package" is used to describe the artifact you publish to and install from your package manager (i.e. something that you would `pip install`). + +{% include requirement/MUST id="python-namespaces-prefix" %} implement your library as a sub-package of the `azure` root namespace. + +> Note: You MUST NOT use `microsoft` as your root namespace. If you need to include `microsoft` in the namespace (e.g. because of policy requirements for extensions to other projects such as `opentelemetry`), you should concatenate it with the package specific namespace with an underscore (e.g. `microsoft_myservice`). You may still use `microsoft-myservice` as the distribution package name in this scenario. + +{% include requirement/MUST id="python-namespaces-naming" %} pick a package name that allows the consumer to tie the namespace to the service being used. As a default, use the compressed service name at the end of the namespace. The namespace does NOT change when the branding of the product changes. Avoid the use of marketing names that may change. + +A compressed service name is the service name without spaces. It may further be shortened if the shortened version is well known in the community. For example, “Azure Media Analytics” would have a compressed service name of `mediaanalytics`, and “Azure Service Bus” would become `servicebus`. Separate words using an underscore if necessary. For example, `mediaanalytics` could be separated into `media_analytics` + +{% include requirement/MAY id="python-namespaces-grouping" %} include a group name segment in your namespace (for example, `azure..`) if your service or family of services have common behavior (for example, shared authentication types). + +{% include requirement/MUST id="python-namespaces-grouping-dont-introduce-new-packages" %} avoid introducing new distribution packages that only differ in name. For existing packages, this means that you should not change the name of the package just to introduce a group name. + +If you want to use a group name segment, use one of the following groups: + +{% include tables/data_namespaces.md %} + +{% include requirement/MUST id="python-namespaces-mgmt" %} place management (Azure Resource Manager) APIs in the `mgmt` group. Use the grouping `azure.mgmt.` for the namespace. Since more services require control plane APIs than data plane APIs, other namespaces may be used explicitly for control plane only. + +{% include requirement/MUST id="python-namespaces-register" %} register the chosen namespace with the [Architecture Board]. Open an [issue] to request the namespace. See [the registered namespace list](registered_namespaces.html) for a list of the currently registered namespaces. + +{% include requirement/MUST id="python-namespaces-async" %} use an `.aio` suffix added to the namespace of the sync client for async clients. + +Example: + +```python +# Yes: +from azure.exampleservice.aio import ExampleServiceClient + +# No: Wrong namespace, wrong client name... +from azure.exampleservice import AsyncExampleServiceClient +``` + +#### Example Namespaces + +Here are some examples of namespaces that meet these guidelines: + +- `azure.storage.blob` +- `azure.keyvault.certificates` +- `azure.ai.textanalytics` +- `azure.mgmt.servicebus` ### Async support @@ -105,9 +623,9 @@ The `asyncio` library has been available since Python 3.4, and the `async`/`awai {% include requirement/MUST id="python-client-sync-async" %} provide both sync and async versions of your APIs -{% include requirement/MUST id="python-client-async-keywords" %} use the `async`/`await` keywords (requires Python 3.5+). Don't use the [yield from coroutine or asyncio.coroutine](https://docs.python.org/3.4/library/asyncio-task.html) syntax. +{% include requirement/MUST id="python-client-async-keywords" %} use the `async`/`await` keywords (requires Python 3.5+). Do not use the [yield from coroutine or asyncio.coroutine](https://docs.python.org/3.4/library/asyncio-task.html) syntax. -{% include requirement/MUST id="python-client-separate-sync-async" %} provide two separate client classes for synchronous and asynchronous operations. Don't combine async and sync operations in the same class. +{% include requirement/MUST id="python-client-separate-sync-async" %} provide two separate client classes for synchronous and asynchronous operations. Do not combine async and sync operations in the same class. ```python # Yes @@ -140,161 +658,199 @@ class ExampleClient(object): # Don't mix'n match with different method names Example: -|Sync/async|Namespace|Package name|Client name| +|Sync/async|Namespace|Distribution package name|Client name| |-|-|-|-| |Sync|`azure.sampleservice`|`azure-sampleservice`|`azure.sampleservice.SampleServiceClient`| |Async|`azure.sampleservice.aio`|`azure-sampleservice-aio`|`azure.sampleservice.aio.SampleServiceClient`| {% include requirement/MUST id="python-client-namespace-sync" %} use the same namespace for the synchronous client as the synchronous version of the package with `.aio` appended. +Example: + +```python +from azure.storage.blob import BlobServiceClient # Sync client + +from azure.storage.blob.aio import BlobServiceClient # Async client +``` + {% include requirement/SHOULD id="python-client-separate-async-pkg" %} ship a separate package for async support if the async version requires additional dependencies. {% include requirement/MUST id="python-client-same-pkg-name-sync-async" %} use the same name for the asynchronous version of the package as the synchronous version of the package with `-aio` appended. {% include requirement/MUST id="python-client-async-http-stack" %} use [`aiohttp`](https://aiohttp.readthedocs.io/en/stable/) as the default HTTP stack for async operations. Use `azure.core.pipeline.transport.AioHttpTransport` as the default `transport` type for the async client. -### Hierarchical services +## Azure SDK distribution packages -Many services have resources with nested child (or sub) resources. For example, Azure Storage provides an account that contains zero or more containers, which in turn contains zero or more blobs. +### Packaging -{% include requirement/MUST id="python-client-hierarchy" %} create a client type corresponding to each level in the hierarchy except for leaf resource types. You **may** omit creating a client type for leaf node resources. +{% include requirement/MUST id="python-packaging-name" %} name your package after the namespace of your main client class. For example, if your main client class is in the `azure.data.tables` namespace, your package name should be azure-data-tables. -{% include requirement/MUST id="python-client-hier-creation" %} make it possible to directly create clients for each level in the hierarchy. The constructor can be called directly or via the parent. +{% include requirement/MUST id="python-packaging-name-allowed-chars" %} use all lowercase in your package name with a dash (-) as a separator. -```python -class ChildClient: - # Yes: - __init__(self, parent, name, credentials, **kwargs) ... +{% 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. -class ChildClient: - # Yes: - __init__(self, url, credentials, **kwargs) ... -``` +{% include requirement/MUST id="python-packaging-follow-repo-rules" %} follow the specific package guidance from the [azure-sdk-packaging wiki](https://github.com/Azure/azure-sdk-for-python/wiki/Azure-packaging) -{% include requirement/MUST id="python-client-hier-vend" %} provide a `get__client(self, name, **kwargs)` method to retrieve a client for the named child. The method must not make a network call to verify the existence of the child. +{% include requirement/MUST id="python-packaging-follow-python-rules" %} follow the [namespace package recommendations for Python 3.x](https://docs.python.org/3/reference/import.html#namespace-packages) for packages that only need to target 3.x. -{% include requirement/MUST id="python-client-hier-create" %} provide method `create_(...)` that creates a child resource. The method **should** return a client for the newly created child resource. +{% include requirement/MUST id="python-general-supply-sdist" %} provide both source distributions (`sdist`) and wheels. -{% include requirement/SHOULD id="python-client-hier-delete" %} provide method `delete_(...)` that deletes a child resource. +{% include requirement/MUST id="python-general-pypi" %} publish both source distributions (`sdist`) and wheels to PyPI. -### Service operations +{% include requirement/MUST id="python-general-wheel-behavior" %} test correct behavior for both CPython and PyPy for [pure](https://packaging.python.org/guides/distributing-packages-using-setuptools/#id75) and [universal](https://packaging.python.org/guides/distributing-packages-using-setuptools/#universal-wheels) Python wheels. -{% include requirement/SHOULD id="python-client-service-verbs" %} prefer the usage one of the preferred verbs for method names. +{% include requirement/MUST id="python-packaging-nspkg" %} depend on `azure-nspkg` for Python 2.x. -|Verb|Parameters|Returns|Comments| -|-|-|-|-| -|`create_\`|key, item, `[allow_overwrite=True]`|Created item|Create new item. Fails if item already exists.| -|`upsert_\`|key, item|item|Create new item, or update existing item. Verb is primarily used in database-like services | -|`set_\`|key, item|item|Create new item, or update existing item. Verb is primarily used for dictionary-like properties of a service | -|`update_\`|key, partial item|item|Fails if item doesn't exist. | -|`replace_\`|key, item|item|Completely replaces an existing item. Fails if the item doesn't exist. | -|`append_\`|item|item|Add item to a collection. Item will be added last. | -|`add_\`|index, item|item|Add item to a collection. Item will be added at the given index. | -|`get_\`|key|item|Raises an exception if item doesn't exist | -|`list_\`||`azure.core.Pageable[Item]`|Return an iterable of items. Returns iterable with no items if no items exist (doesn't return `None` or throw)| -|`\\_exists`|key|`bool`|Return `True` if the item exists. Must raise an exception if the method failed to determine if the item exists (for example, the service returned an HTTP 503 response)| -|`delete_\`|key|`None`|Delete an existing item. Must succeed even if item didn't exist.| -|`remove_\`|key|removed item or `None`|Remove a reference to an item from a collection. This method doesn't delete the actual item, only the reference.| +{% include requirement/MUST id="python-packaging-group-nspkg" %} depend on `azure--nspkg` for Python 2.x if you are using namespace grouping. -{% include requirement/MUST id="python-client-standardize-verbs" %} standardize verb prefixes outside the list of preferred verbs for a given service across language SDKs. If a verb is called `download` in one language, we should avoid naming it `fetch` in another. +{% include requirement/MUST id="python-packaging-init" %} include `__init__.py` for the namespace(s) in sdists -{% include requirement/MUST id="python-lro-prefix" %} prefix methods with `begin_` for long running operations. Long running operations *must* return a [Poller](#python-core-protocol-lro-poller) object. +#### Service-specific common library code -{% include requirement/MUST id="python-client-service-args" %} support the common arguments for service operations: +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. -|Name|Description|Applies to|Notes| -|-|-|-|-| -|`timeout`|Timeout in seconds|All service methods| -|`headers`|Custom headers to include in the service request|All requests|Headers are added to all requests made (directly or indirectly) by the method.| -|`continuation_token`|Opaque token indicating the first page to retrieve. Retrieved from a previous `Paged` return value.|`list` operations.| -|`client_request_id`|Caller specified identification of the request.|Service operations for services that allow the client to send a client-generated correlation ID.|Examples of this include `x-ms-client-request-id` headers.|The client library **must** use this value if provided, or generate a unique value for each request when not specified.| -|`response_hook`|`callable` that is called with (response, headers) for each operation.|All service methods| +{% include requirement/MUST id="python-commonlib-approval" %} gain [Architecture Board] approval prior to implementing a common library. -{% include requirement/MUST id="python-client-splat-args" %} accept a Mapping (`dict`-like) object in the same shape as a serialized model object for parameters. +{% 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. -```python -# Yes: -class Model(object): +A common library will only be approved if: - def __init__(self, name, size): - self.name = name - self.size = size +* 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 -def do_something(model: "Model"): - ... +Let's take two examples: -do_something(Model(name='a', size=17)) # Works -do_something({'name': 'a', 'size', '17'}) # Does the same thing... -``` +1. Implementing two Cognitive Services client libraries, we find that they both rely on the same business logic. This is a candidate for choosing a common library. -{% include requirement/MUST id="python-client-flatten-args" %} use "flattened" named arguments for `update_` methods. **May** additionally take the whole model instance as a named parameter. If the caller passes both a model instance and individual key=value parameters, the explicit key=value parameters override whatever was specified in the model instance. +2. Two Cognitive Services client libraries have models (data classes) that are the same in shape, but has no or minimal logic associated with them. This is not a good candidate for a shared library. Instead, implement two separate classes. -```python -class Model(object): +### Package Versioning - def __init__(self, name, size, description): - self.name = name - self.size = size - self.description = description +{% include requirement/MUST id="python-versioning-semver" %} use [semantic versioning](https://semver.org) for your package. -class Client(object): +{% include requirement/MUST id="python-versioning-beta" %} use the `bN` pre-release segment for [beta releases](https://www.python.org/dev/peps/pep-0440/#pre-releases). - def update_model(self, name=None, size=None, model=None): ... +Don't use pre-release segments other than the ones defined in [PEP440](https://www.python.org/dev/peps/pep-0440) (`aN`, `bN`, `rcN`). Build tools, publication tools, and index servers may not sort the versions correctly. -model = Model(name='hello', size=4711, description='This is a description...') +{% include requirement/MUST id="python-versioning-changes" %} change the version number if *anything* changes in the library. -client.update_model(model=model, size=4712) # Will send a request to the service to update the model's size to 4712 -model.description = 'Updated' -model.size = -1 -# Will send a request to the service to update the model's size to 4713 and description to 'Updated' -client.update_model(name='hello', size=4713, model=model) -``` +{% include requirement/MUST id="python-versioning-patch" %} increment the patch version if only bug fixes are added to the package. -TODO: If there are design considerations to call out that parallel the xxOptions parameters, please add those here. e.g. per the Python API Design Training Article Anna put together at **github.com/Azure/azure-sdk-pr/blob/master/training/azure-sdk-apis/getting-started/design-the-api/design-the-api-python.md#the-get-request**, what should be positional parameters vs. kwargs? +{% include requirement/MUST id="python-verioning-minor" %} increment the minor version if any new functionality is added to the package. -#### Response formats +{% include requirement/MUST id="python-versioning-apiversion" %} increment (at least) the minor version if the default REST API version is changed, even if there's no public API change to the library. -Requests to the service fall into two basic groups - methods that make a single logical request, or a deterministic sequence of requests. An example of a single logical request is a request that may be retried inside the operation. An example of a deterministic sequence of requests is a paged operation. +{% 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. -The logical entity is a protocol neutral representation of a response. For HTTP, the logical entity may combine data from headers, body, and the status line. For example, you may wish to expose an `ETag` header as a property on the logical entity. +{% 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]. -{% include requirement/MUST id="python-response-logical-entity" %} optimize for returning the logical entity for a given request. The logical entity MUST represent the information needed in the 99%+ case. +{% include requirement/MUST id="python-versioning-major-cross-languages" %} select a version number greater than the highest version number of any other released Track 1 package for the service in any other scope or language. -{% include requirement/MUST id="python-response-paged-protocol" %} return a value that implements the [Paged protocol](#python-core-protocol-paged) for operations that return collections. The [Paged protocol](#python-core-protocol-paged) allows the user to iterate through all items in a returned collection, and also provides a method that gives access to individual pages. +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. -{% include requirement/MUST id="python-lro-poller" %} return a value that implements the [Poller protocol](#python-core-protocol-lro-poller) for long running operations. +### Dependencies -TODO: Please include a code sample for return types +{% include requirement/MUST id="python-dependencies-approved-list" %} only pick external dependencies from the following list of well known packages for shared functionality: -TODO: Please add a section on Conditional Requests - much of this is documented [here](https://azure.github.io/azure-sdk/general_design.html#conditional-requests), but could be made Python-specific as needed. +{% include_relative approved_dependencies.md %} -## Models +{% 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/MUST id="python-models-repr" %} implement `__repr__` for model types. The representation **must** include the type name and any key properties (that is, properties that help identify the model instance). +{% include requirement/MUSTNOT id="python-dependencies-vendor" %} vendor dependencies unless approved by the [Architecture Board]. -{% include requirement/MUST id="python-models-repr-length" %} truncate the output of `__repr__` after 1024 characters. +When you vendor a dependency in Python, you include the source from another package as if it was part of your package. -TODO: Please add discussion similar to the [Model Type discussion from the General Guidelines](https://azure.github.io/azure-sdk/general_design.html#model-types), including the naming table if relevant to Python, or an alternate one specific to Python. Per the Python API Design Training Article linked above, when should a model type be replaced with a dictionary in the Azure SDK? +{% 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. -TODO: Please include a code sample for model types. +Only applications are expected to pin exact dependencies. Libraries are not. A library should use a [compatible release](https://www.python.org/dev/peps/pep-0440/#compatible-release) identifier for the dependency. -TODO: If you do anything specific to model extensible enums in Python that an API designer should know, please call this out. +### Binary extensions (native code) -## Authentication +{% include requirement/MUST id="python-native-approval" %} seek approval by the [Architecture Board] before implementing a binary extension. -{% include requirement/MUST id="python-auth-credential-azure-core" %} use the credentials classes in `azure-core` whenever possible. +{% include requirement/MUST id="python-native-plat-support" %} support Windows, Linux (manylinux - see [PEP513](https://www.python.org/dev/peps/pep-0513/), [PEP571](https://www.python.org/dev/peps/pep-0571/)), and MacOS. Support the earliest possible manylinux to maximize your reach. -{% include requirement/MAY id="python-auth-service-credentials" %} add additional credential types if required by the service. Contact @adparch for guidance if you believe you have need to do so. +{% include requirement/MUST id="python-native-arch-support" %} support both x86 and x64 architectures. -TODO: update contact information for the architecture board to reflect the new process. +{% include requirement/MUST id="python-native-charset-support" %} support both Unicode and ASCII versions of CPython 2.7. -{% include requirement/MUST id="python-auth-service-support" %} support all authentication methods that the service supports. +### Docstrings + +{% include requirement/MUST id="python-docstrings-pydocs" %} follow the [documentation guidelines](http://aka.ms/pydocs) unless explicitly overridden in this document. + +{% include requirement/MUST id="python-docstrings-all" %} provide docstrings for all public modules, types, constants and functions. + +{% include requirement/MUST id="python-docstrings-kwargs" %} document any `**kwargs` directly consumed by a method. You may refer to the signature of a called method if the `**kwargs` are passed through. + +Example: +```python +def request(method, url, headers, **kwargs): ... + +def get(*args, **kwargs): + "Calls `request` with the method "GET" and forwards all other arguments." + return request("GET", *args, **kwargs) +``` + +{% include requirement/MUST id="python-docstrings-exceptions" %} document exceptions that may be raised explicitly in the method and any exceptions raised by the called method. + +#### Code snippets -TODO: please include a code sample for authentication +{% include requirement/MUST id="python-snippets-include" %} include example code snippets alongside your library's code within the repository. The snippets should clearly and succinctly demonstrate the operations most developers need to perform with your library. Include snippets for every common operation, and especially for those that are complex or might otherwise be difficult for new users of your library. At a bare minimum, include snippets for the champion scenarios you've identified for the library. + +{% include requirement/MUST id="python-snippets-build" %} build and test your example code snippets using the repository's continuous integration (CI) to ensure they remain functional. + +{% include requirement/MUST id="python-snippets-docstrings" %} include the example code snippets in your library's docstrings so they appear in its API reference. If the language and its tools support it, ingest these snippets directly into the API reference from within the docstrings. Each sample should be a valid `pytest`. + +Use the `literalinclude` directive in Python docstrings to instruct Sphinx to [ingest the snippets automatically][1]. + +{% include requirement/MUSTNOT id="python-snippets-combinations" %} combine more than one operation in a code snippet unless it's required for demonstrating the type or member, or it's *in addition to* existing snippets that demonstrate atomic operations. For example, a Cosmos DB code snippet should not include both account and container creation operations--create two different snippets, one for account creation, and one for container creation. ## Repository Guidelines -TODO: Please include a section on Python Samples. +### Documentation style + +There are several documentation deliverables that must be included in or as a companion to your client library. Beyond complete and helpful API documentation within the code itself (docstrings), you need a great README and other supporting documentation. + +* `README.md` - Resides in the root of your library's directory within the SDK repository; includes package installation and client library usage information. ([example][https://github.com/Azure/azure-sdk-for-python/blob/master/sdk/appconfiguration/azure-appconfiguration/README.md]) +* `API reference` - Generated from the docstrings in your code; published on docs.microsoft.com. +* `Code snippets` - Short code examples that demonstrate single (atomic) operations for the champion scenarios you've identified for your library; included in your README, docstrings, and Quickstart. +* `Quickstart` - Article on docs.microsoft.com that is similar to but expands on the README content; typically written by your service's content developer. +* `Conceptual` - Long-form documentation like Quickstarts, Tutorials, How-to guides, and other content on docs.microsoft.com; typically written by your service's content developer. + +{% include requirement/MUST id="python-docs-content-dev" %} include your service's content developer in the adparch review for your library. To find the content developer you should work with, check with your team's Program Manager. + +{% include requirement/MUST id="python-docs-contributor-guide" %} follow the [Azure SDK Contributors Guide]. (MICROSOFT INTERNAL) + +{% include requirement/MUST id="python-docs-style-guide" %} adhere to the specifications set forth in the Microsoft style guides when you write public-facing documentation. This applies to both long-form documentation like a README and the docstrings in your code. (MICROSOFT INTERNAL) + +* [Microsoft Writing Style Guide]. +* [Microsoft Cloud Style Guide]. + +{% include requirement/SHOULD id="python-docs-into-silence" %} attempt to document your library into silence. Preempt developers' usage questions and minimize GitHub issues by clearly explaining your API in the docstrings. Include information on service limits and errors they might hit, and how to avoid and recover from those errors. + +As you write your code, *doc it so you never hear about it again.* The less questions you have to answer about your client library, the more time you have to build new features for your service. + +### Samples + +Code samples are small applications that demonstrate a certain feature that is relevant to the client library. Samples allow developers to quickly understand the full usage requirements of your client library. Code samples shouldn't be any more complex than they needed to demonstrate the feature. Don't write full applications. Samples should have a high signal to noise ratio between useful code and boilerplate code for non-related reasons. + +{% include requirement/MUST id="python-samples-include-them" %} include code samples alongside your library's code within the repository. The samples should clearly and succinctly demonstrate the code most developers need to write with your library. Include samples for all common operations. Pay attention to operations that are complex or might be difficult for new users of your library. Include samples for the champion scenarios you've identified for the library. + +{% include requirement/MUST id="python-samples-location" %} place code samples within the /samples directory within the client library root directory. The samples will be packaged into the resulting distribution package. + +{% include requirement/MUST id="python-samples-runnable" %} ensure that each sample file is runnable. + +{% include requirement/MUST id="python-samples-coding-style" %} use Python 3 conventions when creating samples. Do not include Python 2 compatibility code (e.g. using [`six`](https://six.readthedocs.io)) since that will distract from what you are trying to present. Avoid using features newer than the Python 3 baseline support. The current supported Python version is 3.6. + +{% include requirement/MUST id="python-samples-grafting" %} ensure that code samples can be easily grafted from the documentation into a users own application. For example, don't rely on variable declarations in other samples. + +{% include requirement/MUST id="python-samples-readability" %} write code samples for ease of reading and comprehension over code compactness and efficiency. + +{% include requirement/MUST id="python-samples-platform-support" %} ensure that samples can run in Windows, macOS, and Linux development environments. + +{% include requirement/MUSTNOT id="python-snippets-no-combinations" %} combine multiple scenarios in a code sample unless it's required for demonstrating the type or member. For example, a Cosmos DB code sample doesn't include both account and container creation operations. Create a sample for account creation, and another sample for container creation. + +Combined scenarios require knowledge of additional operations that might be outside their current focus. The developer must first understand the code surrounding the scenario they're working on, and can't copy and paste the code sample into their project. {% include refs.md %} {% include_relative refs.md %} diff --git a/docs/python/implementation.md b/docs/python/implementation.md index c368c5a166d..aab0c2eb987 100644 --- a/docs/python/implementation.md +++ b/docs/python/implementation.md @@ -6,47 +6,11 @@ folder: python sidebar: general_sidebar --- -## Configuration +## API implementation -{% 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. - -```python -# No: -def get_thing(name: "str") -> "str": - url = f'https:///things/{name}' - return requests.get(url).json() +### Service client -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:///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 +#### Http pipeline 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. @@ -54,129 +18,114 @@ Since the client library generally wraps one or more HTTP requests, it's importa {% include requirement/SHOULD id="python-network-use-policies" %} include the following policies in the HTTP pipeline: +- Unique Request ID (`azure.core.pipeline.policies.RequestIdPolicy`) +- Headers (`azure.core.pipeline.policies.HeadersPolicy`) - Telemetry (`azure.core.pipeline.policies.UserAgentPolicy`) -- Unique Request ID +- Proxy (`azure.core.pipeline.policies.ProxyPolicy`) +- Content decoding (`azure.core.pipeline.policies.ContentDecodePolicy`) - Retry (`azure.core.pipeline.policies.RetryPolicy` and `azure.core.pipeline.policies.AsyncRetryPolicy`) -- Credentials -- Response downloader -- Distributed tracing +- Credentials (e.g. `BearerTokenCredentialPolicy`, `AzureKeyCredentialPolicy`, etc) +- Distributed tracing (`azure.core.pipeline.policies.DistributedTracingPolicy`) - 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: +```python -{% include_relative approved_dependencies.md %} +from azure.core.pipeline import Pipeline + +from azure.core.pipeline.policies import ( + BearerTokenCredentialPolicy, + ContentDecodePolicy, + DistributedTracingPolicy, + HeadersPolicy, + HttpLoggingPolicy, + NetworkTraceLoggingPolicy, + UserAgentPolicy, +) + +class ExampleClient(object): + + ... + + def _create_pipeline(self, credential, base_url=None, **kwargs): + transport = kwargs.get('transport') or RequestsTransport(**kwargs) + + try: + policies = kwargs['policies'] + except KeyError: + scope = base_url.strip("/") + "/.default" + if hasattr(credential, "get_token"): + credential_policy = BearerTokenCredentialPolicy(credential, scope) + else: + raise ValueError( + "Please provide an instance from azure-identity or a class that implement the 'get_token protocol" + ) + policies = [ + HeadersPolicy(**kwargs), + UserAgentPolicy(**kwargs), + ContentDecodePolicy(**kwargs), + RetryPolicy(**kwargs), + credential_policy, + HttpLoggingPolicy(**kwargs), + DistributedTracingPolicy(**kwargs), + NetworkTraceLoggingPolicy(**kwargs) + ] + + return Pipeline(transport, policies) -{% 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]. +##### Custom policies -When you vendor a dependency in Python, you include the source from another package as if it was part of your package. +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/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. +{% include requirement/SHOULD id="python-pipeline-core-policies" %} use the policy implementations in `azure-core` whenever possible. -Applications are expected to pin exact dependencies. Libraries aren't. A library should use a [compatible release](https://www.python.org/dev/peps/pep-0440/#compatible-release) identifier for the dependency. +{% include requirement/MUST id="python-custom-policy-review" %} review the proposed policy with the Azure SDK [Architecture Board]. There may already be an existing policy that can be modified/parameterized to satisfy your need. -## Service-specific common library code +{% include requirement/MUST id="python-custom-policy-base-class" %} derive from [HTTPPolicy](https://azuresdkdocs.blob.core.windows.net/$web/python/azure-core/1.9.0/azure.core.pipeline.policies.html#azure.core.pipeline.policies.HTTPPolicy)/[AsyncHTTPPolicy](https://azuresdkdocs.blob.core.windows.net/$web/python/azure-core/1.9.0/azure.core.pipeline.policies.html#azure.core.pipeline.policies.AsyncHTTPPolicy) (if you need to make network calls) or [SansIOHTTPPolicy](https://azuresdkdocs.blob.core.windows.net/$web/python/azure-core/1.9.0/azure.core.pipeline.policies.html#azure.core.pipeline.policies.SansIOHTTPPolicy) (if you do not). -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-custom-policy-thread-safe" %} ensure thread-safety for custom policies. A practical consequence of this is that you should keep any per-request or connection bookkeeping data in the context rather than in the policy instance itself. -{% include requirement/MUST id="python-commonlib-approval" %} gain [Architecture Board] approval prior to implementing a common library. +{% 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-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-pipeline-policy-namespace" %} add the policies to the `azure..pipeline.policies` namespace. -{% include requirement/MUST id="python-commonlib-namespace" %} store the common library in the same namespace as the associated client libraries. +#### Service Methods -A common library will only be approved if: +##### Parameter validation -* 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. +{% include requirement/MUSTNOT id="python-client-parameter-validation" %} use `isinstance` to validate parameter value types other than for [built-in types](https://docs.python.org/3/library/stdtypes.html) (e.g. `str` etc). For other types, use [structural type checking]. -Let's take two examples: +### Supporting types -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. +{% include requirement/MUST id="python-models-repr" %} implement `__repr__` for model types. The representation **must** include the type name and any key properties (that is, properties that help identify the model instance). -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. +{% include requirement/MUST id="python-models-repr-length" %} truncate the output of `__repr__` after 1024 characters. -## Error handling +##### Extensible enumerations -{% 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. +Any Enums defined in the SDK should be interchangable with case-insensitive strings. This is achieved by using the `CaseInsensitiveEnumMeta` class defined in `azure-core`. ```python -# Yes -try: - resource = client.create_resource(name) -except azure.core.errors.ResourceExistsException: - print('Failed - we need to fix this!') +from enum import Enum +from six import with_metaclass -# 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: +from azure.core import CaseInsensitiveEnumMeta -```python -# 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") +class MyCustomEnum(with_metaclass(CaseInsensitiveEnumMeta, str, Enum)): + FOO = 'foo' + BAR = 'bar' ``` -{% 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. +### SDK Feature implementation -{% 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. +#### Configuration -{% 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](https://docs.python.org/3/library/exceptions.html) will suffice. - -{% include requirement/MUST id="python-errors-use-chaining" %} allow exception chaining to include the original source of the error. - -```python -# Yes: -try: - # do something -except: - raise MyOwnErrorWithNoContext() - -# No: -success = True -try: - # do something -except: - success = False -if not success: - raise MyOwnErrorWithNoContext() +{% include requirement/MUST id="python-envvars-global" %} honor the following environment variables for global configuration settings: -# No: -success = True -try: - # do something -except: - raise MyOwnErrorWithNoContext() from None -``` +{% include tables/environment_variables.md %} -## Logging +#### Logging {% include requirement/MUST id="python-logging-usage" %} use Pythons standard [logging module](https://docs.python.org/3/library/logging.html). @@ -214,9 +163,7 @@ The `DEBUG` logging level is intended for developers or system administrators to You can determine the logging level for a given logger by calling [`logging.Logger.isEnabledFor`](https://docs.python.org/3/library/logging.html#logging.Logger.isEnabledFor). -## Distributed tracing - -> **DRAFT** section +#### Distributed tracing {% 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`. @@ -226,28 +173,86 @@ You can determine the logging level for a given logger by calling [`logging.Logg {% include requirement/MUST id="python-tracing-propagate" %} propagate tracing context on each outgoing service request. -## Azure Core +#### Telemetry -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. +Client library usage telemetry is used by service teams (not consumers) to monitor what SDK language, client library version, and language/platform info a client is using to call into their service. Clients can prepend additional information indicating the name and version of the client application. -### HTTP pipeline +{% include requirement/MUST id="python-http-telemetry-useragent" %} send telemetry information in the [User-Agent header] using the following format: -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. +``` +[ ]azsdk-python-/ +``` -For more information on the Python implementation of the pipeline, see the [documentation](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/core/azure-core). +- ``: optional application-specific string. May contain a slash, but must not contain a space. The string is supplied by the user of the client library, e.g. "AzCopy/10.0.4-Preview" +- ``: client library (distribution) package name as it appears to the developer, replacing slashes with dashes and removing the Azure indicator. For example, "azure-keyvault-secrets" would specify "azsdk-python-keyvault-secrets". +- ``: the version of the package. Note: this is not the version of the service +- ``: information about the currently executing language runtime and OS, e.g. "Python/3.8.4 (Windows-10-10.0.19041-SP0)" -#### Custom Policies +For example, if we re-wrote `AzCopy` in Python using the Azure Blob Storage client library, we may end up with the following user-agent strings: -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. +- (Python) `AzCopy/10.0.4-Preview azsdk-python-storage/4.0.0 Python/3.7.3 (Ubuntu; Linux x86_64; rv:34.0)` -{% include requirement/SHOULD id="python-pipeline-core-policies" %} use the policy implementations in `azure-core` whenever possible. +The `azure.core.pipeline.policies.UserAgentPolicy` will provide this functionality if added to the HttpPipeline. -{% 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/SHOULD id="python-azurecore-http-telemetry-dynamic" %} send additional (dynamic) telemetry information as a semi-colon separated set of key-value types in the `X-MS-AZSDK-Telemetry` header. For example: -{% include requirement/MUST id="python-pipeline-policy-namespace" %} add the policies to the `azure..pipeline.policies` namespace. +```http +X-MS-AZSDK-Telemetry: class=BlobClient;method=DownloadFile;blobType=Block +``` + +The content of the header is a semi-colon key=value list. The following keys have specific meaning: + +* `class` is the name of the type within the client library that the consumer called to trigger the network operation. +* `method` is the name of the method within the client library type that the consumer called to trigger the network operation. + +Any other keys that are used should be common across all client libraries for a specific service. **DO NOT** include personally identifiable information (even encoded) in this header. Services need to configure log gathering to capture the `X-MS-SDK-Telemetry` header in such a way that it can be queried through normal analytics systems. + +##### Considerations for clients not using the UserAgentPolicy from azure-core + +{% include requirement/MUST id="python-azurecore-http-telemetry-appid" %} allow the consumer of the library to set the application ID by passing in an `application_id` parameter to the service client constructor. This allows the consumer to obtain cross-service telemetry for their app. + +{% include requirement/MUST id="python-azurecore-http-telemetry-appid-length" %} enforce that the application ID is no more than 24 characters in length. Shorter application IDs allows service teams to include diagnostic information in the "platform information" section of the user agent, while still allowing the consumer to obtain telemetry information for their own application. + +## Testing + +{% include requirement/MUST id="python-testing-pytest" %} use [pytest](https://docs.pytest.org/en/latest/) as the test framework. + +{% include requirement/SHOULD id="python-testing-async" %} use [pytest-asyncio](https://github.com/pytest-dev/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](https://github.com/Azure/azure-sdk-for-python/tree/master/tools/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. + +## Code Analysis and Style Tools + +{% include requirement/MUST id="python-tooling-pylint" %} use [pylint](https://www.pylint.org/) for your code. Use the pylintrc file in the [root of the repository](https://github.com/Azure/azure-sdk-for-python/blob/master/pylintrc). + +{% include requirement/MUST id="python-tooling-flake8" %} use [flake8-docstrings](https://gitlab.com/pycqa/flake8-docstrings) to verify doc comments. + +{% include requirement/MUST id="python-tooling-black" %} use [Black](https://black.readthedocs.io/en/stable/) for formatting your code. + +{% include requirement/SHOULD id="python-tooling-mypy" %} use [MyPy](https://mypy.readthedocs.io/en/latest/) to statically check the public surface area of your library. + +You don't need to check non-shipping code such as tests. + +## Making use of 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](https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/core/azure-core). ### Protocols +Many of the protocols mandated by the design guidelines have default implementations in `azure-core`. + #### LROPoller ```python @@ -255,7 +260,7 @@ T = TypeVar("T") class LROPoller(Protocol): def result(self, timeout=None) -> T: - """ Retreive the final result of the long running operation. + """ Retrieve 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. @@ -280,22 +285,24 @@ class LROPoller(Protocol): ... ``` -`azure.core.LROPoller` implements the `LROPoller` protocol. +`azure.core.polling.LROPoller` implements the `LROPoller` protocol. -#### Paged +#### ItemPaged ```python T = TypeVar("T") class ByPagePaged(Protocol, Iterable[Iterable[T]]): continuation_token: "str" -class Paged(Protocol, Iterable[T]): +class ItemPaged(Protocol, Iterable[T]): continuation_token: "str" def by_page(self) -> ByPagePaged[T] ... ``` -`azure.core.Paged` implements the `Paged` protocol. +`azure.core.ItemPaged` implements the `ItemPaged` protocol. + +See the [ItemPaged](#PythonPagingDesign) protocol for additional information. #### DiagnosticsResponseHook @@ -306,87 +313,221 @@ class ResponseHook(Protocol): ``` -## Versioning +## Python language and code style -{% include requirement/MUST id="python-versioning-semver" %} use [semantic versioning](https://semver.org) for your package. +{% include requirement/MUST id="python-codestyle-pep8" %} follow the general guidelines in [PEP8](https://www.python.org/dev/peps/pep-0008/) unless explicitly overridden in this document. -{% include requirement/MUST id="python-versioning-beta" %} use the `bN` pre-release segment for [beta releases](https://www.python.org/dev/peps/pep-0440/#pre-releases). +{% include requirement/MUSTNOT id="python-codestyle-idiomatic" %} "borrow" coding paradigms from other languages. -Don't use pre-release segments other than the ones defined in [PEP440](https://www.python.org/dev/peps/pep-0440) (`aN`, `bN`, `rcN`). Build tools, publication tools, and index servers may not sort the versions correctly. +For example, no matter how common Reactive programming is in the Java community, it's still unfamiliar for most Python developers. -{% include requirement/MUST id="python-versioning-changes" %} change the version number if *anything* changes in the library. +{% include requirement/MUST id="python-codestyle-consistency" %} favor consistency with other Python components over other libraries for the same service. -{% include requirement/MUST id="python-versioning-patch" %} increment the patch version if only bug fixes are added to the package. +It's more likely that a developer will use many different libraries using the same language than a developer will use the same service from many different languages. -{% include requirement/MUST id="python-verioning-minor" %} increment the minor version if any new functionality is added to the package. +### Error handling -{% 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/MUST id="python-errors-use-chaining" %} use exception chaining to include the original source of the error when catching and raising new exceptions. -{% 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. +```python +# Yes: +try: + # do something + something() +except: + # __context__ will be set correctly + raise MyOwnErrorWithNoContext() -{% 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]. +# No: +success = True +try: + # do something + something() +except: + success = False +if not success: + # __context__ is lost... + raise MyOwnErrorWithNoContext() +``` -{% include requirement/MUST id="python-versioning-major" %} select a version number greater than the highest version number of any other released Track 1 package for the service in any other scope or language. +### Naming conventions -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. +{% include requirement/MUST id="python-codestyle-vars-naming" %} use snake_case for variable, function, and method names: -### REST API method versioning +```python +# Yes: +service_client = ServiceClient() -{% include requirement/MUST id="python-versioning-latest-service-api" %} use the latest service protocol version when making requests. +service_client.list_things() -{% include requirement/MUST id="python-versioning-select-service-api" %} allow a client application to specify an earlier version of the service protocol. +def do_something(): + ... -## Packaging +# No: +serviceClient = ServiceClient() -{% include requirement/MUST id="python-packaging-name" %} name your package after the namespace of your main client class. +service_client.listThings() -{% include requirement/MUST id="python-packaging-name-allowed-chars" %} use all lowercase in your package name with a dash (-) as a separator. +def DoSomething(): + ... +``` -{% 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-codestyle-type-naming" %} use Pascal case for types: -{% include requirement/MUST id="python-packaging-follow-repo-rules" %} follow the specific package guidance from the [azure-sdk-packaging wiki](https://github.com/Azure/azure-sdk-for-python/wiki/Azure-packaging) +```python +# Yes: +class ThisIsCorrect(object): + pass -{% include requirement/MUST id="python-packaging-follow-python-rules" %} follow the [namespace package recommendations for Python 3.x](https://docs.python.org/3/reference/import.html#namespace-packages) for packages that only need to target 3.x. +# No: +class this_is_not_correct(object): + pass -{% include requirement/MUST id="python-packaging-nspkg" %} depend on `azure-nspkg` for Python 2.x. +# No: +class camelCasedTypeName(object): + pass +``` -{% include requirement/MUST id="python-packaging-init" %} include `__init__.py` for the namespace(s) in sdists +{% include requirement/MUST id="python-codestyle-const-naming" %} use ALL CAPS for constants: -### Binary extensions +```python +# Yes: +MAX_SIZE = 4711 -{% include requirement/MUST id="python-native-approval" %} be approved by the [Architecture Board]. +# No: +max_size = 4711 -{% include requirement/MUST id="python-native-plat-support" %} support Windows, Linux (manylinux - see [PEP513](https://www.python.org/dev/peps/pep-0513/), [PEP571](https://www.python.org/dev/peps/pep-0571/)), and MacOS. Support the earliest possible manylinux to maximize your reach. +# No: +MaxSize = 4711 +``` -{% include requirement/MUST id="python-native-arch-support" %} support both x86 and x64 architectures. +{% include requirement/MUST id="python-codestyle-module-naming" %} use snake_case for module names. -{% include requirement/MUST id="python-native-charset-support" %} support unicode and ASCII versions of CPython 2.7. +### Method signatures -## Testing +{% include requirement/MUSTNOT id="python-codestyle-static-methods" %} use static methods ([`staticmethod`](https://docs.python.org/3/library/functions.html#staticmethod)). Prefer module level functions instead. -{% include requirement/MUST id="python-testing-pytest" %} use [pytest](https://docs.pytest.org/en/latest/) as the test framework. +Static methods are rare and usually forced by other libraries. -{% include requirement/SHOULD id="python-testing-async" %} use [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) for testing of async code. +{% include requirement/MUSTNOT id="python-codestyle-properties" %} use simple getter and setter functions. Use properties instead. -{% include requirement/MUST id="python-testing-live" %} make your scenario tests runnable against live services. Strongly consider using the [Python Azure-DevTools](https://github.com/Azure/azure-python-devtools) package for scenario tests. +```python +# Yes +class GoodThing(object): -{% include requirement/MUST id="python-testing-record" %} provide recordings to allow running tests offline/without an Azure subscription + @property + def something(self): + """ Example of a good read-only property.""" + return self._something -{% include requirement/MUST id="python-testing-parallel" %} support simultaneous test runs in the same subscription. +# No +class BadThing(object): -{% include requirement/MUST id="python-testing-independent" %} make each test case independent of other tests. + def get_something(self): + """ Example of a bad 'getter' style method.""" + return self._something +``` -## Recommended Tools +{% include requirement/SHOULDNOT id="python-codestyle-long-args" %} have methods that require more than five positional parameters. Optional/flag parameters can be accepted using keyword-only arguments, or `**kwargs`. -{% include requirement/MUST id="python-tooling-pylint" %} use [pylint](https://www.pylint.org/) for your code. Use the pylintrc file in the [root of the repository](https://github.com/Azure/azure-sdk-for-python/blob/master/pylintrc). +See TODO: insert link for general guidance on positional vs. optional parameters here. -{% include requirement/MUST id="python-tooling-flake8" %} use [flake8-docstrings](https://gitlab.com/pycqa/flake8-docstrings) to verify doc comments. +{% include requirement/MUST id="python-codestyle-optional-args" %} use keyword-only arguments for optional or less-often-used arguments for modules that only need to support Python 3. -{% include requirement/MUST id="python-tooling-black" %} use [Black](https://black.readthedocs.io/en/stable/) for formatting your code. +```python +# Yes +def foo(a, b, *, c, d=None): + # Note that I can even have required keyword-only arguments... + ... +``` -{% include requirement/SHOULD id="python-tooling-mypy" %} use [MyPy](https://mypy.readthedocs.io/en/latest/) to statically check the public surface area of your library. +{% include requirement/MUST id="python-codestyle-kwargs" %} use keyword-only arguments for arguments that have no obvious ordering. -You don't need to check non-shipping code such as tests. +```python +# Yes - `source` and `dest` have logical order, `recurse` and `overwrite` do not. +def copy(source, dest, *, recurse=False, overwrite=False) ... + + +# No +def copy(source, dest, recurse=False, overwrite=False) ... +``` + +{% include requirement/MUST id="python-codestyle-positional-params" %} specify the parameter name when calling methods with more than two required positional parameters. + +```python +def foo(a, b, c): + pass + + +def bar(d, e): + pass + + +# Yes: +foo(a=1, b=2, c=3) +bar(1, 2) +bar(e=3, d=4) + +# No: +foo(1, 2, 3) +``` + +{% include requirement/MUST id="python-codestyle-optional-param-calling" %} specify the parameter name for optional parameters when calling functions. + +```python +def foo(a, b=1, c=None): + pass + + +# Yes: +foo(1, b=2, c=3) + +# No: +foo(1, 2, 3) +``` + +### Public vs "private" + +{% include requirement/MUST id="python-codestyle-private-api" %} use a single leading underscore to indicate that a name isn't part of the public API. Non-public APIs aren't guaranteed to be stable. + +{% include requirement/MUSTNOT id="python-codestyle-double-underscore" %} use leading double underscore prefixed method names unless name clashes in the inheritance hierarchy are likely. Name clashes are rare. + +{% include requirement/MUST id="python-codestyle-public-api" %} add public methods and types to the module's `__all__` attribute. + +{% include requirement/MUST id="python-codestyle-interal-module" %} use a leading underscore for internal modules. You **may** omit a leading underscore if the module is a submodule of an internal module. + +```python +# Yes: +azure.exampleservice._some_internal_module + +# Yes - some_internal_module is still considered internal since it is a submodule of an internal module: +azure.exampleservice._internal.some_internal_module + +# No - some_internal_module is considered public: +azure.exampleservice.some_internal_module +``` + +### Types (or not) + +{% include requirement/MUST id="python-codestyle-structural-subtyping" %} prefer structural subtyping and protocols over explicit type checks. + +{% include requirement/MUST id="python-codestyle-abstract-collections" %} derive from the abstract collections base classes `collections.abc` (or `collections` for Python 2.7) to provide custom mapping types. + +{% include requirement/MUST id="python-codestyle-pep484" %} provide type hints [PEP484](https://www.python.org/dev/peps/pep-0484/) for publicly documented classes and functions. + +- See the [suggested syntax for Python 2.7 and 2.7-3.x straddling code](https://www.python.org/dev/peps/pep-0484/#suggested-syntax-for-python-2-7-and-straddling-code) for guidance for Python 2.7 compatible code. Do not do this for code that is Python 3 specific (e.g. `async` clients.) + +### Threading + +{% include requirement/MUST id="python-codestyle-thread-affinity" %} maintain thread affinity for user-provided callbacks unless explicitly documented to not do so. + +{% include requirement/MUST id="python-codestyle-document-thread-safety" %} explicitly include the fact that a method (function/class) is thread safe in its documentation. + +Examples: [`asyncio.loop.call_soon_threadsafe`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.call_soon_threadsafe), [`queue`](https://docs.python.org/3/library/queue.html) + +{% include requirement/SHOULD id="python-codestyle-use-executor" %} allow callers to pass in an [`Executor`](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor) instance rather than defining your own thread or process management for parallelism. + +You may do your own thread management if the thread isn't exposed to the caller in any way. For example, the `LROPoller` implementation uses a background poller thread. {% include refs.md %} {% include_relative refs.md %} diff --git a/docs/python/introduction.md b/docs/python/introduction.md deleted file mode 100644 index 672374e61a9..00000000000 --- a/docs/python/introduction.md +++ /dev/null @@ -1,291 +0,0 @@ ---- -title: "Python Guidelines: Introduction" -keywords: guidelines python -permalink: python_introduction.html -folder: python -sidebar: general_sidebar ---- - -## Design principles - -The Azure SDK should be designed to enhance the productivity of developers connecting to Azure services. Other qualities (such as completeness, extensibility, and performance) are important but secondary. Productivity is achieved by adhering to the principles described below: - -### Idiomatic - -* The SDK should follow the general design guidelines and conventions for the target language. It should feel natural to a developer in the target language. -* We embrace the ecosystem with its strengths and its flaws. -* We work with the ecosystem to improve it for all developers. - -### Consistent - -* Client libraries should be consistent within the language, consistent with the service and consistent between all target languages. In cases of conflict, consistency within the language is the highest priority and consistency between all target languages is the lowest priority. -* Service-agnostic concepts such as logging, HTTP communication, and error handling should be consistent. The developer should not have to relearn service-agnostic concepts as they move between client libraries. -* Consistency of terminology between the client library and the service is a good thing that aids in diagnosability. -* All differences between the service and client library must have a good (articulated) reason for existing, rooted in idiomatic usage rather than whim. -* The Azure SDK for each target language feels like a single product developed by a single team. -* There should be feature parity across target languages. This is more important than feature parity with the service. - -### Approachable - -* We are experts in the supported technologies so our customers, the developers, don't have to be. -* Developers should find great documentation (hero tutorial, how to articles, samples, and API documentation) that makes it easy to be successful with the Azure service. -* Getting off the ground should be easy through the use of predictable defaults that implement best practices. Think about progressive concept disclosure. -* The SDK should be easily acquired through the most normal mechanisms in the target language and ecosystem. -* Developers can be overwhelmed when learning new service concepts. The core use cases should be discoverable. - -### Diagnosable - -* The developer should be able to understand what is going on. -* It should be discoverable when and under what circumstances a network call is made. -* Defaults are discoverable and their intent is clear. -* Logging, tracing, and exception handling are fundamental and should be thoughtful. -* Error messages should be concise, correlated with the service, actionable, and human readable. Ideally, the error message should lead the consumer to a useful action that they can take. -* Integrating with the preferred debugger for the target language should be easy. - -### Dependable - -* Breaking changes are more harmful to a user's experience than most new features and improvements are beneficial. -* Incompatibilities should never be introduced deliberately without thorough review and very strong justification. -* Do not rely on dependencies that can force our hand on compatibility. - -## Python Design Principles - -Use the guiding principles in the Zen of Python [Zen of Python] when making design trade-offs: - -``` - Beautiful is better than ugly. - Explicit is better than implicit. - Simple is better than complex. - Complex is better than complicated. - Flat is better than nested. - Sparse is better than dense. - Readability counts. - Special cases aren't special enough to break the rules. - Although practicality beats purity. - Errors should never pass silently. - Unless explicitly silenced. - In the face of ambiguity, refuse the temptation to guess. - There should be one-- and preferably only one --obvious way to do it. - Although that way may not be obvious at first unless you're Dutch. - Now is better than never. - Although never is often better than *right* now. - If the implementation is hard to explain, it's a bad idea. - If the implementation is easy to explain, it may be a good idea. - Namespaces are one honking great idea -- let's do more of those! -``` - -## General Guidelines - -{% include requirement/MUST id="python-general-follow-general-guidelines" %} follow the [General Azure SDK Guidelines]. - -{% include requirement/MUST id="python-general-monorepo" %} locate all source code in the [azure/azure-sdk-for-python] GitHub repository. - -{% include requirement/MUST id="python-general-engsys" %} follow Azure SDK engineering systems guidelines for working in the [azure/azure-sdk-for-python] GitHub repository. - -## Supported python versions - -{% include requirement/MUST id="python-general-version-support" %} support Python 2.7 and 3.5.3+. - -{% include requirement/SHOULD id="python-general-universal-pkg" %} provide a [universal package] that works on all supported versions of Python, unless there's a compelling reason to have separate Python2 and Python3 packages. - -For example, if you depend on different external packages for Python2 and Python3, and neither external dependency is available for both Python versions. - -{% include requirement/MUST id="python-general-supply-sdist" %} provide both source distributions (`sdist`) and wheels. - -{% include requirement/MUST id="python-general-pypi" %} publish both source distributions (`sdist`) and wheels to PyPI. - -{% include requirement/MUST id="python-general-wheel-behavior" %} test correct behavior for both CPython and PyPy for [pure](https://packaging.python.org/guides/distributing-packages-using-setuptools/#id75) and [universal](https://packaging.python.org/guides/distributing-packages-using-setuptools/#universal-wheels) Python wheels. - -For more information on packaging naming, see the [Packaging] section. - -## Code style - -{% include requirement/MUST id="python-codestyle-pep8" %} follow the general guidelines in [PEP8](https://www.python.org/dev/peps/pep-0008/) unless explicitly overridden in this document. - -{% include requirement/MUSTNOT id="python-codestyle-idiomatic" %} "borrow" coding paradigms from other languages. - -For example, no matter how common Reactive programming is in the Java community, it's still unfamiliar for most Python developers. - -{% include requirement/MUST id="python-codestyle-consistency" %} favor consistency with other Python components over other libraries for the same service. - -It's more likely that a developer will use many different libraries using the same language than a developer will use the same service from many different languages. - -### Naming conventions - -{% include requirement/MUST id="python-codestyle-vars-naming" %} use snake_case for variable, function, and method names: - -```python -# Yes: -service_client = ServiceClient() - -service_client.list_things() - -def do_something(): - ... - -# No: -serviceClient = ServiceClient() - -service_client.listThings() - -def DoSomething(): - ... -``` - -{% include requirement/MUST id="python-codestyle-type-naming" %} use Pascal case for types: - -```python -# Yes: -class ThisIsCorrect(object): - pass - -# No: -class this_is_not_correct(object): - pass - -# No: -class camelCasedTypeName(object): - pass -``` - -{% include requirement/MUST id="python-codestyle-const-naming" %} use ALL CAPS for constants: - -```python -# Yes: -MAX_SIZE = 4711 - -# No: -max_size = 4711 - -# No: -MaxSize = 4711 -``` - -{% include requirement/MUST id="python-codestyle-module-naming" %} use snake_case for module names. - -### Method signatures - -{% include requirement/MUSTNOT id="python-codestyle-static-methods" %} use static methods ([`staticmethod`](https://docs.python.org/3/library/functions.html#staticmethod)). Prefer module level functions instead. - -Static methods are rare and usually forced by other libraries. - -{% include requirement/MUSTNOT id="python-codestyle-properties" %} use simple getter and setter functions. Use properties instead. - -```python -# Yes -class GoodThing(object): - - @property - def something(self): - """ Example of a good read-only property.""" - return self._something - -# No -class BadThing(object): - - def get_something(self): - """ Example of a bad 'getter' style method.""" - return self._something -``` - -{% include requirement/SHOULDNOT id="python-codestyle-long-args" %} have methods that require more than five positional parameters. Optional/flag parameters can be accepted using keyword-only arguments, or `**kwargs`. - -{% include requirement/MUST id="python-codestyle-optional-args" %} use keyword-only arguments for optional or less-often-used arguments for modules that only need to support Python 3. - -```python -# Yes -def foo(a, b, *, c, d=None): - # Note that I can even have required keyword-only arguments... - ... -``` - -{% include requirement/MUST id="python-codestyle-kwargs" %} use keyword-only arguments for arguments that have no obvious ordering. - -```python -# Yes - `source` and `dest` have logical order, `recurse` and `overwrite` do not. -def copy(source, dest, *, recurse=False, overwrite=False) ... - - -# No -def copy(source, dest, recurse=False, overwrite=False) ... -``` - -{% include requirement/MUST id="python-codestyle-positional-params" %} specify the parameter name when calling methods with more than two required positional parameters. - -```python -def foo(a, b, c): - pass - - -def bar(d, e): - pass - - -# Yes: -foo(a=1, b=2, c=3) -bar(1, 2) -bar(e=3, d=4) - -# No: -foo(1, 2, 3) -``` - -{% include requirement/MUST id="python-codestyle-optional-param-calling" %} specify the parameter name for optional parameters when calling functions. - -```python -def foo(a, b=1, c=None): - pass - - -# Yes: -foo(1, b=2, c=3) - -# No: -foo(1, 2, 3) -``` - -### Public vs "private" - -{% include requirement/MUST id="python-codestyle-private-api" %} use a single leading underscore to indicate that a name isn't part of the public API. Non-public APIs aren't guaranteed to be stable. - -{% include requirement/MUSTNOT id="python-codestyle-double-underscore" %} use leading double underscore prefixed method names unless name clashes in the inheritance hierarchy are likely. Name clashes are rare. - -{% include requirement/MUST id="python-codestyle-public-api" %} add public methods and types to the module's `__all__` attribute. - -{% include requirement/MUST id="python-codestyle-interal-module" %} use a leading underscore for internal modules. You **may** omit a leading underscore if the module is a submodule of an internal module. - -```python -# Yes: -azure.exampleservice._some_internal_module - -# Yes - some_internal_module is still considered internal since it is a submodule of an internal module: -azure.exampleservice._internal.some_internal_module - -# No - some_internal_module is considered public: -azure.exampleservice.some_internal_module -``` - -### Types (or not) - -{% include requirement/MUST id="python-codestyle-structural-subtyping" %} prefer structural subtyping and protocols over explicit type checks. - -{% include requirement/MUST id="python-codestyle-abstract-collections" %} derive from the abstract collections base classes `collections.abc` (or `collections` for Python 2.7) to provide custom mapping types. - -{% include requirement/MUST id="python-codestyle-pep484" %} provide type hints [PEP484](https://www.python.org/dev/peps/pep-0484/) for publicly documented classes and functions. - -- See the [suggested syntax for Python 2.7 and 2.7-3.x straddling code](https://www.python.org/dev/peps/pep-0484/#suggested-syntax-for-python-2-7-and-straddling-code) for guidance for Python 2.7 compatible code. - -### Threading - -{% include requirement/MUST id="python-codestyle-thread-affinity" %} maintain thread affinity for user-provided callbacks unless explicitly documented to not do so. - -{% include requirement/MUST id="python-codestyle-document-thread-safety" %} explicitly include the fact that a method (function/class) is thread safe in its documentation. - -Examples: [`asyncio.loop.call_soon_threadsafe`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.call_soon_threadsafe), [`queue`](https://docs.python.org/3/library/queue.html) - -{% include requirement/SHOULD id="python-codestyle-use-executor" %} allow callers to pass in an [`Executor`](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor) instance rather than defining your own thread or process management for parallelism. - -You may do your own thread management if the thread isn't exposed to the caller in any way. For example, the `LROPoller` implementation uses a background poller thread. - -{% include refs.md %} -{% include_relative refs.md %} \ No newline at end of file diff --git a/docs/python/refs.md b/docs/python/refs.md index 81b5e748b90..971315e6569 100644 --- a/docs/python/refs.md +++ b/docs/python/refs.md @@ -1,7 +1,12 @@ [Zen of Python]: https://www.python.org/dev/peps/pep-0020/ [universal package]: https://packaging.python.org/guides/distributing-packages-using-setuptools/#id75 +[Architecture board]: https://azure.github.io/azure-sdk/policies_reviewprocess.html +[issue]: https://github.com/azure/azure-sdk/issues [Packaging]: python_implementation.html#packaging [HTTP pipeline]: python_implementation.html#azure-core -[azure/azure-sdk-for-python]: https://github.com/azure/azure-sdk-for-python \ No newline at end of file +[PagingDesign]: python_design.html#methods-returning-collections-paging +[azure/azure-sdk-for-python]: https://github.com/azure/azure-sdk-for-python +[positional and keyword-only arguments]: python_implementation.html#python-codestyle-long-args +[structural type checking]: python_implementation.html#types-or-not \ No newline at end of file diff --git a/docs/redirects/python-guidelines.md b/docs/redirects/python-guidelines.md index b442b990f8e..52766d7bdbf 100644 --- a/docs/redirects/python-guidelines.md +++ b/docs/redirects/python-guidelines.md @@ -1,5 +1,5 @@ --- permalink: /python/guidelines/index.html layout: redirect -redirect_uri: /python_introduction.html +redirect_uri: /python_design.html --- \ No newline at end of file