Skip to content

Commit

Permalink
Add profile to SDK (Azure#2247)
Browse files Browse the repository at this point in the history
* Add profile to SDK

* Feedback from Derek

* Doc update

* Update Storage with new Profile

* Update Compute with new Profile

* Update Resource with new Profile

* Improve ResourceGroupPreparer to provide the id

* Migrate all Resources tests to new framework

* Generic ChangeLog with Profiles

* Add RequestUrlNormalizer to SDK processors
  • Loading branch information
lmazuel authored Apr 3, 2018
1 parent 96bb51e commit a7eabc2
Show file tree
Hide file tree
Showing 39 changed files with 931 additions and 611 deletions.
127 changes: 127 additions & 0 deletions azure-common/azure/profiles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#-------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
#--------------------------------------------------------------------------
from enum import Enum

class ProfileDefinition(object):
"""Allow to define a custom Profile definition.
Note::
The dict format taken as input is yet to be confirmed and should
*not* be considered as stable in the current implementation.
:param dict profile_dict: A profile dictionnary
:param str label: A label for pretty printing
"""
def __init__(self, profile_dict, label=None):
self._profile_dict = profile_dict
self._label = label

@property
def label(self):
"""The label associated to this profile definition.
"""
return self._label

def __repr__(self):
return self._label if self._label else self._profile_dict.__repr__()

def get_profile_dict(self):
"""Return the current profile dict.
This is internal information, and content should not be considered stable.
"""
return self._profile_dict


class DefaultProfile(object):
"""Store a default profile.
:var ProfileDefinition profile: The default profile as class attribute
"""
profile = None

def use(self, profile):
"""Define a new default profile."""
if not isinstance(profile, (KnownProfiles, ProfileDefinition)):
raise ValueError("Can only set as default a ProfileDefinition or a KnownProfiles")
type(self).profile = profile

def definition(self):
return type(self).profile

class KnownProfiles(Enum):
"""This defines known Azure Profiles.
There is two meta-profiles:
- latest : will always use latest available api-version on each package
- default : mutable, will define profile automatically for all packages
If you change default, this changes all created packages on the fly to
this profile. This can be used to switch a complete set of API Version
without re-creating all clients.
"""

# default - This is a meta-profile and point to another profile
default = DefaultProfile()
# latest - This is a meta-profile and does not contain definitions
latest = ProfileDefinition(None, "latest")
v2017_03_09_profile = ProfileDefinition(
{
"azure.mgmt.compute.ComputeManagementClient": {
None: "2016-03-30"
},
"azure.mgmt.network.NetworkManagementClient": {
None: "2015-06-15"
},
"azure.mgmt.storage.StorageManagementClient": {
None: "2016-01-01"
},
"azure.mgmt.resource.policy.PolicyClient": {
None: "2015-10-01-preview"
},
"azure.mgmt.resource.locks.ManagementLockClient": {
None: "2015-01-01"
},
"azure.mgmt.resource.links.ManagementLinkClient": {
None: "2016-09-01"
},
"azure.mgmt.resource.resources.ResourceManagementClient": {
None: "2016-02-01"
},
"azure.mgmt.resource.subscriptions.SubscriptionClient": {
None: "2016-06-01"
}
},
"2017-03-09-profile"
)

def __init__(self, profile_definition):
self._profile_definition = profile_definition

def use(self, profile):
if self is not type(self).default:
raise ValueError("use can only be used for `default` profile")
self.value.use(profile)

def definition(self):
if self is not type(self).default:
raise ValueError("use can only be used for `default` profile")
return self.value.definition()

@classmethod
def from_name(cls, profile_name):
if profile_name == "default":
return cls.default
for profile in cls:
if isinstance(profile.value, ProfileDefinition) and profile.value.label == profile_name:
return profile
raise ValueError("No profile called {}".format(profile_name))


# Default profile is floating "latest"
KnownProfiles.default.use(KnownProfiles.latest)
81 changes: 81 additions & 0 deletions azure-common/azure/profiles/multiapiclient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#-------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
#--------------------------------------------------------------------------
from . import KnownProfiles, ProfileDefinition

class InvalidMultiApiClientError(Exception):
"""If the mixin is not used with a compatible class.
"""
pass

class MultiApiClientMixin(object):
"""Mixin that contains multi-api version profile management.
To use this mixin, a client must define two class attributes:
- LATEST_PROFILE : a ProfileDefinition correspond to latest profile
- _PROFILE_TAG : a tag that filter a full profile for this particular client
This should not be used directly and will only provide private methods.
"""

def __init__(self, api_version=None, profile=KnownProfiles.default, **kwargs):

try:
type(self).LATEST_PROFILE
except AttributeError:
raise InvalidMultiApiClientError("To use this mixin, main client MUST define LATEST_PROFILE class attribute")

try:
type(self)._PROFILE_TAG
except AttributeError:
raise InvalidMultiApiClientError("To use this mixin, main client MUST define _PROFILE_TAG class attribute")

if api_version and profile is not KnownProfiles.default:
raise ValueError("Cannot use api-version and profile parameters at the same time")

if api_version:
self.profile = ProfileDefinition({
self._PROFILE_TAG: {
None: api_version
}},
self._PROFILE_TAG + " " + api_version
)
elif isinstance(profile, dict):
self.profile = ProfileDefinition({
self._PROFILE_TAG: profile,
},
self._PROFILE_TAG + " dict"
)
if api_version:
self.profile._profile_dict[self._PROFILE_TAG][None] = api_version
else:
self.profile = profile

def _get_api_version(self, operation_group_name):
current_profile = self.profile
if self.profile is KnownProfiles.default:
current_profile = KnownProfiles.default.value.definition()

if current_profile is KnownProfiles.latest:
current_profile = self.LATEST_PROFILE
elif isinstance(current_profile, KnownProfiles):
current_profile = current_profile.value
elif isinstance(current_profile, ProfileDefinition):
pass # I expect that
else:
raise ValueError("Cannot determine a ProfileDefinition from {}".format(self.profile))

local_profile_dict = current_profile.get_profile_dict()
if self._PROFILE_TAG not in local_profile_dict:
raise ValueError("This profile doesn't define {}".format(self._PROFILE_TAG))

local_profile = local_profile_dict[self._PROFILE_TAG]
if operation_group_name in local_profile:
return local_profile[operation_group_name]
try:
return local_profile[None]
except KeyError:
raise ValueError("This profile definition does not contain a default API version")

80 changes: 80 additions & 0 deletions azure-common/tests/test_profile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#-------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
#--------------------------------------------------------------------------
from azure.profiles import ProfileDefinition, KnownProfiles
from azure.profiles.multiapiclient import MultiApiClientMixin

import pytest

def test_profile_from_string():
profile_from_string = KnownProfiles.from_name("2017-03-09-profile")
assert profile_from_string is KnownProfiles.v2017_03_09_profile

with pytest.raises(ValueError):
KnownProfiles.from_name("blablabla")

def test_default_profile():
with pytest.raises(ValueError):
KnownProfiles.default.use("This is not a profile")

def test_multiapi_client():

class TestClient(MultiApiClientMixin):
DEFAULT_API_VERSION = "2216-08-09"
_PROFILE_TAG = "azure.mgmt.compute.ComputeManagementClient"
LATEST_PROFILE = ProfileDefinition({
_PROFILE_TAG: {
None: DEFAULT_API_VERSION
}},
_PROFILE_TAG + " latest"
)

def __init__(self, api_version=None, profile=KnownProfiles.default):
super(TestClient, self).__init__(api_version=api_version, profile=profile)

def operations(self):
return self._get_api_version("operations")

# By default, use latest
client = TestClient()
assert client.operations() == TestClient.DEFAULT_API_VERSION

# Dynamically change to a new profile
KnownProfiles.default.use(KnownProfiles.v2017_03_09_profile)
assert client.operations() == "2016-03-30"

# I ask explicitly latest, where the default is not that
client = TestClient(profile=KnownProfiles.latest)
assert client.operations() == TestClient.DEFAULT_API_VERSION

# Bring back default to latest for next tests
KnownProfiles.default.use(KnownProfiles.latest)

# I asked explicily a specific profile, must not be latest
client = TestClient(profile=KnownProfiles.v2017_03_09_profile)
assert client.operations() == "2016-03-30"

# I refuse api_version and profile at the same time
# https://github.com/Azure/azure-sdk-for-python/issues/1864
with pytest.raises(ValueError):
TestClient(api_version="something", profile=KnownProfiles.latest)

# If I provide only api_version, this creates a profile with just that
client = TestClient(api_version="2666-05-15")
assert client.operations() == "2666-05-15"

# I can specify old profile syntax with dict
client = TestClient(profile={
"operations": "1789-07-14"
})
assert client.operations() == "1789-07-14"

# If I give a profile definition with no default api-version
# and I call a method not define in the profile, this fails
client = TestClient(profile={
"operations2": "1789-07-14"
})
with pytest.raises(ValueError):
client.operations() == "1789-07-14"
8 changes: 8 additions & 0 deletions azure-mgmt-compute/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
Release History
===============

XXXXXXXXXXXX
++++++++++++

**Features**

- All clients now support Azure profiles.


4.0.0rc1 (2018-03-21)
+++++++++++++++++++++

Expand Down
Loading

0 comments on commit a7eabc2

Please sign in to comment.