-
Notifications
You must be signed in to change notification settings - Fork 108
adlfs user agent #501
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
base: main
Are you sure you want to change the base?
adlfs user agent #501
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. I just had a couple of suggestions.
adlfs/spec.py
Outdated
@@ -2055,21 +2062,26 @@ def connect_client(self): | |||
account_url=self.fs.account_url, | |||
credential=cred, | |||
_location_mode=self.fs.location_mode, | |||
user_agent=self._user_agent, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I missed that we need to set the user_agent
in this class as well as in the file system class when we talked about it offline...
Given that we need to duplicate this logic across classes along with if branch statements, it is probably worth we try to consolidate client creation to module level helper function at this point. To do this, let's:
- Define a top-level
_USER_AGENT
that we can use as the constant for the adlfs user agent. - Define a top-level
_create_aio_blob_service_client()
function that requires anaccount_url
and optional acceptscredential
andlocation_mode
. It will use_USER_AGENT
as part of creating the client - Define a top-level
_create_aio_blob_service_client_from_connection_string()
function that requires just aconnection_string
. It will also set_USER_AGENT
as part of creating the client.
I think in making this refactor it is going to much more manageable adding the user agent in and plumb in more configurations in the future.
adlfs/tests/test_spec.py
Outdated
@@ -2045,3 +2048,15 @@ def test_open_file_x(storage: azure.storage.blob.BlobServiceClient, tmpdir): | |||
with fs.open("data/afile", "xb") as f: | |||
pass | |||
assert fs.cat_file("data/afile") == b"data" | |||
|
|||
|
|||
def test_user_agent(storage: azure.storage.blob.BlobServiceClient, mocker): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if we could expand + parameterize this test case to assert user agent being set for:
- Both the file system and file classes
- Both client creation approaches i.e., using
from_connection_string
and using the initializer
I think this is important given the amount of branching logic when it comes to client creation so that there is coverage for the various ways clients are created in adlfs
1a52ede
to
0b9ec60
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking good. I mainly focused on the implementation. I still want to take a deeper pass on the tests to understand how the scaffolding and fixtures are set up with adlfs as part of reviewing the PR's tests.
if credential is not None: | ||
return AIOBlobServiceClient( | ||
account_url=account_url, | ||
credential=credential, | ||
_location_mode=location_mode, | ||
user_agent=_get_user_agent(), | ||
) | ||
elif location_mode is not None: | ||
return AIOBlobServiceClient( | ||
account_url=account_url, | ||
credential=None, | ||
_location_mode=location_mode, | ||
user_agent=_get_user_agent(), | ||
) | ||
else: | ||
return AIOBlobServiceClient( | ||
account_url=account_url, | ||
user_agent=_get_user_agent(), | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this factory, it would be interesting to see if we can consolidate the actual client instantiation to one instance. To do this we should be able to leverage keyword arguments, for example:
service_client_kwargs = {
"account_url": account_url,
"user_agent": _get_user_agent(),
}
if credential is not None:
service_client_kwargs["credentials"] = credentials
if location_mode is not None:
service_client_kwargs["_location_mode"] = location_mode
return AIOBlobServiceClient(**service_client_kwargs)
The main reason that I'm suggesting we do this is it reduces the verbosity and makes it easier to follow/add to the logic when there is only one path to instantiating the client.
def _create_aio_blob_service_client( | ||
account_url: str, | ||
location_mode: Optional[str] = None, | ||
credential: Optional[str] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm technically this is not the correct type; it can be more than a string. But it looks like the initializer for the filesystem use this type as well. I think it is fine to keep it as is but it may be worth doing a pass at a later time to add correct typing to the project and integrate mypy
into the CI. Mainly the value add is add another level of safety when adding to the project and it allows consumers to use correct types when consuming adlfs.
@@ -73,6 +73,12 @@ | |||
_SOCKET_TIMEOUT_DEFAULT = object() | |||
|
|||
|
|||
def _get_user_agent(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to break the circular loop here, I think it would be worth moving this logic: https://github.com/anjaliratnam-msft/adlfs/blob/f64dff2b2ce62dd2d194e0be02804104b4505d7d/adlfs/__init__.py#L6-L11 to the utils.py
module and then have the __init__.py
import __version__
and version_tuple
and this module can import __version__
. Then we can go back to having a module level _USER_AGENT
constant.
Generally, a utils.py
module will have all of the shared code and not have any intra project imports which is usually a safe approach to avoiding import errors.
assert mock_client.call_args.kwargs["user_agent"] == f"adlfs/{__version__}" | ||
|
||
|
||
@patch("adlfs.spec.AIOBlobServiceClient") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you elaborate on why this patch outside of the mocker
is needed? I'm not sure how well the patch will behave when we patch the class here and then patch the from_connection_string
of the already patched class later on. I'd assume we'd only need one of the patches for the purpose introspection and mocking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I run it on its own the test passes but when I run the whole test directory it fails without patching the class. For some reason it's only this test that's causing issues. I'm pretty sure it's happening because the AIOBlobServiceClient class is getting patched in another test and it's affecting this, but I couldn't figure out where it's coming from.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that sounds like a patch that is not properly being cleaned up. I'll take a look to see if I can find anything
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, in terms of state remaining, we may be running up against the caching feature of fsspec: https://filesystem-spec.readthedocs.io/en/latest/features.html#instance-caching. For any of the calls we make to instantiating an AzureBlobFileSystem
, we should probably throw in an skip_instance_cache=True
so that we actually create a new file system and try to recreate a new service client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Just leaving comments related to the tests.
@@ -2045,3 +2047,100 @@ def test_open_file_x(storage: azure.storage.blob.BlobServiceClient, tmpdir): | |||
with fs.open("data/afile", "xb") as f: | |||
pass | |||
assert fs.cat_file("data/afile") == b"data" | |||
|
|||
|
|||
def test_user_agent_blob_file_connection_str( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For all of these test cases that we are adding, I'm thinking we just move them to a test_user_agent.py
file. This follows the pattern of other features in the project that have several related test cases. This is beneficial in that it will:
- Reduce adding to the cluttering of
test_spec.py
- Makes it easier to run just the user agent tests (e.g.
py.test test_user_agent.py
)
mock_client.assert_called_once() | ||
assert "user_agent" in mock_client.call_args.kwargs | ||
assert mock_client.call_args.kwargs["user_agent"] == f"adlfs/{__version__}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would probably make sense to consolidate these assertions to a helper function (e.g., assert_sets_adlfs_user_agent
) and we can just provide it the mock client method whether it be the mock initializer or the from_connection_string
mock.
mock_client = mocker.patch.object( | ||
AIOBlobServiceClient, "from_connection_string", return_value=mocker.MagicMock() | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Playing around with these mocks, I think it would be worth just having two new patch fixtures: one for the __init__
and one for the from_connection_string()
method e.g.,:
@pytest.fixture()
def mock_from_connection_string(mocker):
return mocker.patch.object(
AIOBlobServiceClient, "from_connection_string", autospec=True,
side_effect=AIOBlobServiceClient.from_connection_string
)
@pytest.fixture()
def mock_service_client_init(mocker):
return mocker.patch.object(
AIOBlobServiceClient, "__init__", autospec=True,
side_effect=AIOBlobServiceClient.__init__
)
This is a pattern used in another test case in test_spec.py
where it effective will allow us to spy the arguments passed in but delegated to the actual method. Right now, in patching the entire class or building up mocks seems like a lot of scaffolding would be required to be able to get a test passing and I'm not sure if the scaffolding makes sense when we are already leveraging Azurite for the fake. With this approach, we should just be able to get what we want, which is capturing what was passed to the various class instantiation methods.
fs = AzureBlobFileSystem( | ||
account_name=storage.account_name, | ||
) | ||
AzureBlobFile(fs, "data/root/a/file.txt", mode="rb") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this test case, I'm thinking we either do the following to test the case where we are not using from_connection_string()
:
- Try to generate a SAS and provide it as a
credential
to the file and file system to make Azurite's authentication happy - Create a new container that allows anonymous reads in Azurite that we reference instead of the
data
container.
The main reason that I think we should explore that approach is that because we are already using Azurite, it seems ideal to try to avoid building up mocks/fakes if we can avoid it to reduce the overall amount of scaffolding needed.
@@ -73,6 +73,12 @@ | |||
_SOCKET_TIMEOUT_DEFAULT = object() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also make sure to add a changelog entry for this update here: https://github.com/fsspec/adlfs/blob/main/CHANGELOG.md
return_value=mock_client_instance, | ||
) | ||
|
||
AzureBlobFileSystem(account_name=storage.account_name, connection_string=CONN_STR) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whenever creating an AzureBlobFileSystem
, we should definitely be setting skip_instance_cache=True
so that we make sure we always construct a new file system for these cases around instantiation
Added adlfs to the user agent to differentiate between adlfs and azure python sdk usage.
Request headers example:
'x-ms-version': 'REDACTED'
'Accept': 'application/xml'
'User-Agent': 'adlfs/2024.12.0 azsdk-python-storage-blob/12.25.1 Python/3.13.5 (Windows-11-10.0.26100-SP0)'
'x-ms-date': 'REDACTED'
'x-ms-client-request-id': '0fbad80d-5059-11f0-9c34-000d3a6d20b2'
'Authorization': 'REDACTED'