Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

{Core} Add dict transformation for typespec generated SDKs #30339

Merged
merged 8 commits into from
Dec 2, 2024

Conversation

evelyn-ys
Copy link
Member

@evelyn-ys evelyn-ys commented Nov 14, 2024

Related command

Description

SDK models have changed when we move from swagger generated to typespec generated. This cause issues for knack.util.todict to do the formatter over command output. We call

result = todict(result, AzCliCommandInvoker.remove_additional_prop_layer)
to convert the command result for easier format. Here's how todict is implemented in knack:

def todict(obj, post_processor=None):  # pylint: disable=too-many-return-statements
    """
    Convert an object to a dictionary. Use 'post_processor(original_obj, dictionary)' to update the
    dictionary in the process
    """
    if isinstance(obj, dict):
        result = {k: todict(v, post_processor) for (k, v) in obj.items()}
        return post_processor(obj, result) if post_processor else result
    if isinstance(obj, list):
        return [todict(a, post_processor) for a in obj]
    if isinstance(obj, Enum):
        return obj.value
    if isinstance(obj, (date, time, datetime)):
        return obj.isoformat()
    if isinstance(obj, timedelta):
        return str(obj)
    if hasattr(obj, '_asdict'):
        return todict(obj._asdict(), post_processor)
    if hasattr(obj, '__dict__'):
        result = {to_camel_case(k): todict(v, post_processor)
                  for k, v in obj.__dict__.items()
                  if not callable(v) and not k.startswith('_')}
        return post_processor(obj, result) if post_processor else result
    return obj

Currently working with swagger generated models, it runs into hasattr(obj, '__dict__') and transforms obj.__dict__.items() one by one if the item isn't private starting with _.

But now for typespec generated models, a new base model is designed to store all data in __dict__['_data']. In such case, we won't get anything by iterating __dict__ as before. The new model provides a as_dict func instead to transform the model object to dict.

Here's an example for swagger generated models(old) and typespec generated models(new)

SecurityDomainUploadStatus (old)
__dict__: {'status': 'Success', 'status_details': 'The resource is active.'}
as_dict(): {'status': 'Success', 'status_details': 'The resource is active.'}

SecurityDomainUploadStatus (new)
__dict__: {'_data': {'status': 'Success', 'status_details': 'The resource is active.'}}
as_dict(): {'status': 'Success', 'status_details': 'The resource is active.'}

There are three workarounds:

This PR works for the third solution.

Testing Guide

History Notes

[Component Name 1] BREAKING CHANGE: az command a: Make some customer-facing breaking change
[Component Name 2] az command b: Add some customer-facing feature


This checklist is used to make sure that common guidelines for a pull request are followed.

Copy link

azure-client-tools-bot-prd bot commented Nov 14, 2024

️✔️AzureCLI-FullTest
️✔️acr
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️acs
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.9
️✔️ams
️✔️latest
️✔️3.12
️✔️3.9
️✔️apim
️✔️latest
️✔️3.12
️✔️3.9
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.9
️✔️appservice
️✔️latest
️✔️3.12
️✔️3.9
️✔️aro
️✔️latest
️✔️3.12
️✔️3.9
️✔️backup
️✔️latest
️✔️3.12
️✔️3.9
️✔️batch
️✔️latest
️✔️3.12
️✔️3.9
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.9
️✔️billing
️✔️latest
️✔️3.12
️✔️3.9
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.9
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.9
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.9
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.9
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.9
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.9
️✔️config
️✔️latest
️✔️3.12
️✔️3.9
️✔️configure
️✔️latest
️✔️3.12
️✔️3.9
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.9
️✔️container
️✔️latest
️✔️3.12
️✔️3.9
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.9
️✔️core
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.9
️✔️databoxedge
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️dls
️✔️latest
️✔️3.12
️✔️3.9
️✔️dms
️✔️latest
️✔️3.12
️✔️3.9
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.9
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.9
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.9
️✔️find
️✔️latest
️✔️3.12
️✔️3.9
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.9
️✔️identity
️✔️latest
️✔️3.12
️✔️3.9
️✔️iot
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️keyvault
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️lab
️✔️latest
️✔️3.12
️✔️3.9
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.9
️✔️maps
️✔️latest
️✔️3.12
️✔️3.9
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.9
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.9
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.9
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.9
️✔️network
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.9
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.9
️✔️profile
️✔️latest
️✔️3.12
️✔️3.9
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.9
️✔️redis
️✔️latest
️✔️3.12
️✔️3.9
️✔️relay
️✔️latest
️✔️3.12
️✔️3.9
️✔️resource
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️role
️✔️latest
️✔️3.12
️✔️3.9
️✔️search
️✔️latest
️✔️3.12
️✔️3.9
️✔️security
️✔️latest
️✔️3.12
️✔️3.9
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.9
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.9
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.9
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.9
️✔️sql
️✔️latest
️✔️3.12
️✔️3.9
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.9
️✔️storage
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.9
️✔️telemetry
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9
️✔️util
️✔️latest
️✔️3.12
️✔️3.9
️✔️vm
️✔️2018-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2019-03-01-hybrid
️✔️3.12
️✔️3.9
️✔️2020-09-01-hybrid
️✔️3.12
️✔️3.9
️✔️latest
️✔️3.12
️✔️3.9

@evelyn-ys evelyn-ys marked this pull request as draft November 14, 2024 01:42
Copy link

azure-client-tools-bot-prd bot commented Nov 14, 2024

️✔️AzureCLI-BreakingChangeTest
️✔️Non Breaking Changes

@yonzhan
Copy link
Collaborator

yonzhan commented Nov 14, 2024

Thank you for your contribution! We will review the pull request and get back to you soon.

@evelyn-ys
Copy link
Member Author

@microsoft-github-policy-service rerun

@evelyn-ys evelyn-ys marked this pull request as ready for review November 19, 2024 07:07
@evelyn-ys
Copy link
Member Author

@microsoft-github-policy-service rerun

Comment on lines +648 to +650
if hasattr(obj, 'as_dict') and not hasattr(obj, '_attribute_map'):
result = {to_camel_case(k): todict(v, post_processor) for k, v in obj.as_dict().items()}
return post_processor(obj, result) if post_processor else result
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good question from @kairu-ms, what if the REST spec defines a asDict property?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now there is no such a property: https://github.com/search?q=repo%3AAzure%2Fazure-rest-api-specs%20asDict&type=code. If there is one in the future, we could rename it.

# This is the only difference with knack.util.todict because for typespec generated SDKs
# The base model stores data in obj.__dict__['_data'] instead of in obj.__dict__
# We need to call obj.as_dict() to extract data for this kind of model
if hasattr(obj, 'as_dict') and not hasattr(obj, '_attribute_map'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach to detect if obj is an SDK model is something similar to

type(obj).__base__.__name__ == 'Model'

str(type(obj).__base__) returns

"<class 'azure.cli.command_modules.keyvault.vendored_sdks.azure_keyvault_securitydomain._model_base.Model'>"

However, this is also not very reliable as the name Model may change at any time.

Using isinstance() is impossible as Model is defined in each SDK, such as azure.cli.command_modules.keyvault.vendored_sdks.azure_keyvault_securitydomain._model_base.Model, instead of in azure.core. There is no single Model that is the base class of all SDK objects.

Also, using isinstance() requires importing the Model class which impairs the performance.

As an example, remove_additional_prop_layer previously imports Model with

from msrest.serialization import Model

#13843 changed it to use a try... catch... strategy:

@staticmethod
def remove_additional_prop_layer(obj, converted_dic):
# Follow EAFP to flatten `additional_properties` auto-generated by SDK
# See https://docs.python.org/3/glossary.html#term-eafp
try:
if 'additionalProperties' in converted_dic and isinstance(obj.additional_properties, dict):
converted_dic.update(converted_dic.pop('additionalProperties'))
except AttributeError:
pass
return converted_dic

Thus, there is no need to import Model for non-Model objects.

@evelyn-ys evelyn-ys merged commit f8cc79f into Azure:dev Dec 2, 2024
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Auto-Assign Auto assign by bot Core CLI core infrastructure
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants