Repository: jschneier/django-storages

Issue:
Add support for azure-storage-blob>=12.0.0
Yesterday version 12.0.0 of [`azure-storage-blob`](https://github.com/Azure/azure-sdk-for-python/releases/tag/azure-storage-blob_12.0.0) was released.

First of all I would like to request pinning versions of required libraries so new releases of these do not instantly break django-storages if they have any breaking changes. In my opinion it is much better to pin the requirements and update manually when needed/requested.

My nightly build using django-storages for Azure failed, and the first issue I found was the renaming of `BlobPermissions` to `BlobSasPermissions`. This must be updated in [azure_storage.py](https://github.com/jschneier/django-storages/blob/master/storages/backends/azure_storage.py#L8)

I have not looked through the rest of the changes yet, but there might be more issues hidden in the release notes. I will try to find the issues and submit a PR.

An immediate quick fix would be to pin `azure-storage-blob` to version `2.1.0`. The [`azure`] module uses `1.3.1`, so that is an option as well.

Release notes for azure-storage-blob 12.0.0:
* [12.0.0](https://github.com/Azure/azure-sdk-for-python/releases/tag/azure-storage-blob_12.0.0)
* [12.0.0b4](https://github.com/Azure/azure-sdk-for-python/releases/tag/azure-storage-blob_12.0.0b4)
* [12.0.0b3](https://github.com/Azure/azure-sdk-for-python/releases/tag/azure-storage-blob_12.0.0b3)
* [12.0.0b2](https://github.com/Azure/azure-sdk-for-python/releases/tag/azure-storage-blob_12.0.0b2)
* [12.0.0b1](https://github.com/Azure/azure-sdk-for-python/releases/tag/azure-storage-blob_12.0.0b1)


Discussion:
+1 👍
There are more changes than just renaming, so I suggest a requirement pin until someone has time to update the code according to 12.0.0 of `azure-storage-blob`. Please see #785 
Yeah, many changes. For now I will just pin to `>1.3.1<2.1`. I'm not a fan of pinning altogether.

Resolving patch:
diff --git a/storages/backends/azure_storage.py b/storages/backends/azure_storage.py
--- a/storages/backends/azure_storage.py
+++ b/storages/backends/azure_storage.py
@@ -2,14 +2,16 @@
 from datetime import datetime, timedelta
 from tempfile import SpooledTemporaryFile
 
-from azure.common import AzureMissingResourceHttpError
-from azure.storage.blob import BlobPermissions, ContentSettings
-from azure.storage.blob.blockblobservice import BlockBlobService
+from azure.core.exceptions import ResourceNotFoundError
+from azure.storage.blob import (
+    BlobClient, BlobSasPermissions, ContainerClient, ContentSettings,
+    generate_blob_sas,
+)
 from django.core.exceptions import SuspiciousOperation
 from django.core.files.base import File
 from django.utils import timezone
 from django.utils.deconstruct import deconstructible
-from django.utils.encoding import filepath_to_uri, force_bytes
+from django.utils.encoding import force_bytes
 
 from storages.base import BaseStorage
 from storages.utils import (
@@ -40,12 +42,9 @@ def _get_file(self):
         if 'r' in self._mode or 'a' in self._mode:
             # I set max connection to 1 since spooledtempfile is
             # not seekable which is required if we use max_connections > 1
-            self._storage.service.get_blob_to_stream(
-                container_name=self._storage.azure_container,
-                blob_name=self._path,
-                stream=file,
-                max_connections=1,
-                timeout=self._storage.timeout)
+            download_stream = self._storage.client.download_blob(
+                self._path, timeout=self._storage.timeout)
+            download_stream.download_to_stream(file, max_concurrency=1)
         if 'r' in self._mode:
             file.seek(0)
 
@@ -122,8 +121,7 @@ def _get_valid_path(s):
 class AzureStorage(BaseStorage):
     def __init__(self, **settings):
         super().__init__(**settings)
-        self._service = None
-        self._custom_service = None
+        self._client = None
 
     def get_default_settings(self):
         return {
@@ -140,51 +138,41 @@ def get_default_settings(self):
             "location": setting('AZURE_LOCATION', ''),
             "default_content_type": 'application/octet-stream',
             "cache_control": setting("AZURE_CACHE_CONTROL"),
-            "is_emulated": setting('AZURE_EMULATED_MODE', False),
-            "endpoint_suffix": setting('AZURE_ENDPOINT_SUFFIX'),
             "sas_token": setting('AZURE_SAS_TOKEN'),
             "custom_domain": setting('AZURE_CUSTOM_DOMAIN'),
             "connection_string": setting('AZURE_CONNECTION_STRING'),
-            "custom_connection_string": setting(
-                'AZURE_CUSTOM_CONNECTION_STRING',
-                setting('AZURE_CONNECTION_STRING'),
-            ),
             "token_credential": setting('AZURE_TOKEN_CREDENTIAL'),
         }
 
-    def _blob_service(self, custom_domain=None, connection_string=None):
-        # This won't open a connection or anything,
-        # it's akin to a client
-        return BlockBlobService(
-            account_name=self.account_name,
-            account_key=self.account_key,
-            sas_token=self.sas_token,
-            is_emulated=self.is_emulated,
-            protocol=self.azure_protocol,
-            custom_domain=custom_domain,
-            connection_string=connection_string,
-            token_credential=self.token_credential,
-            endpoint_suffix=self.endpoint_suffix)
-
-    @property
-    def service(self):
-        if self._service is None:
-            custom_domain = None
-            if self.is_emulated:
-                custom_domain = self.custom_domain
-            self._service = self._blob_service(
-                custom_domain=custom_domain,
-                connection_string=self.connection_string)
-        return self._service
+    def _container_client(self, custom_domain=None, connection_string=None):
+        if custom_domain is None:
+            account_domain = "blob.core.windows.net"
+        else:
+            account_domain = custom_domain
+        if connection_string is None:
+            connection_string = "{}://{}.{}".format(
+                self.azure_protocol,
+                self.account_name,
+                account_domain)
+        credential = None
+        if self.account_key:
+            credential = self.account_key
+        elif self.sas_token:
+            credential = self.sas_token
+        elif self.token_credential:
+            credential = self.token_credential
+        return ContainerClient(
+            connection_string,
+            self.azure_container,
+            credential=credential)
 
     @property
-    def custom_service(self):
-        """This is used to generate the URL"""
-        if self._custom_service is None:
-            self._custom_service = self._blob_service(
+    def client(self):
+        if self._client is None:
+            self._client = self._container_client(
                 custom_domain=self.custom_domain,
-                connection_string=self.custom_connection_string)
-        return self._custom_service
+                connection_string=self.connection_string)
+        return self._client
 
     @property
     def azure_protocol(self):
@@ -219,26 +207,25 @@ def get_available_name(self, name, max_length=_AZURE_NAME_MAX_LEN):
         return super().get_available_name(name, max_length)
 
     def exists(self, name):
-        return self.service.exists(
-            self.azure_container,
-            self._get_valid_path(name),
-            timeout=self.timeout)
+        blob_client = self.client.get_blob_client(self._get_valid_path(name))
+        try:
+            blob_client.get_blob_properties()
+            return True
+        except ResourceNotFoundError:
+            return False
 
     def delete(self, name):
         try:
-            self.service.delete_blob(
-                container_name=self.azure_container,
-                blob_name=self._get_valid_path(name),
+            self.client.delete_blob(
+                self._get_valid_path(name),
                 timeout=self.timeout)
-        except AzureMissingResourceHttpError:
+        except ResourceNotFoundError:
             pass
 
     def size(self, name):
-        properties = self.service.get_blob_properties(
-            self.azure_container,
-            self._get_valid_path(name),
-            timeout=self.timeout).properties
-        return properties.content_length
+        blob_client = self.client.get_blob_client(self._get_valid_path(name))
+        properties = blob_client.get_blob_properties(timeout=self.timeout)
+        return properties.size
 
     def _save(self, name, content):
         cleaned_name = clean_name(name)
@@ -250,13 +237,13 @@ def _save(self, name, content):
             content = content.file
 
         content.seek(0)
-        self.service.create_blob_from_stream(
-            container_name=self.azure_container,
-            blob_name=name,
-            stream=content,
+        self.client.upload_blob(
+            name,
+            content,
             content_settings=ContentSettings(**params),
-            max_connections=self.upload_max_conn,
-            timeout=self.timeout)
+            max_concurrency=self.upload_max_conn,
+            timeout=self.timeout,
+            overwrite=self.overwrite_files)
         return cleaned_name
 
     def _expire_at(self, expire):
@@ -269,17 +256,19 @@ def url(self, name, expire=None):
         if expire is None:
             expire = self.expiration_secs
 
-        make_blob_url_kwargs = {}
+        credential = None
         if expire:
-            sas_token = self.custom_service.generate_blob_shared_access_signature(
-                self.azure_container, name, permission=BlobPermissions.READ, expiry=self._expire_at(expire))
-            make_blob_url_kwargs['sas_token'] = sas_token
+            sas_token = generate_blob_sas(
+                self.account_name,
+                self.azure_container,
+                name,
+                account_key=self.account_key,
+                permission=BlobSasPermissions(read=True),
+                expiry=self._expire_at(expire))
+            credential = sas_token
 
-        return self.custom_service.make_blob_url(
-            container_name=self.azure_container,
-            blob_name=filepath_to_uri(name),
-            protocol=self.azure_protocol,
-            **make_blob_url_kwargs)
+        container_blob_url = self.client.get_blob_client(name).url
+        return BlobClient.from_blob_url(container_blob_url, credential=credential).url
 
     def _get_content_settings_parameters(self, name, content=None):
         params = {}
@@ -311,10 +300,9 @@ def get_modified_time(self, name):
         Returns an (aware) datetime object containing the last modified time if
         USE_TZ is True, otherwise returns a naive datetime in the local timezone.
         """
-        properties = self.service.get_blob_properties(
-            self.azure_container,
+        properties = self.client.get_blob_properties(
             self._get_valid_path(name),
-            timeout=self.timeout).properties
+            timeout=self.timeout)
         if not setting('USE_TZ', False):
             return timezone.make_naive(properties.last_modified)
 
@@ -342,9 +330,8 @@ def list_all(self, path=''):
         # XXX make generator, add start, end
         return [
             blob.name
-            for blob in self.service.list_blobs(
-                self.azure_container,
-                prefix=path,
+            for blob in self.client.list_blobs(
+                name_starts_with=path,
                 timeout=self.timeout)]
 
     def listdir(self, path=''):


Test patch:
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
deleted file mode 100644
diff --git a/tests/integration/migrations/0001_initial.py b/tests/integration/migrations/0001_initial.py
deleted file mode 100644
--- a/tests/integration/migrations/0001_initial.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 2.0.7 on 2018-08-22 08:44
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
-    initial = True
-
-    dependencies = [
-    ]
-
-    operations = [
-        migrations.CreateModel(
-            name='SimpleFileModel',
-            fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('foo_file', models.FileField(upload_to='foo_uploads/')),
-            ],
-        ),
-    ]
diff --git a/tests/integration/migrations/__init__.py b/tests/integration/migrations/__init__.py
deleted file mode 100644
diff --git a/tests/integration/models.py b/tests/integration/models.py
deleted file mode 100644
--- a/tests/integration/models.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from django.db import models
-
-
-class SimpleFileModel(models.Model):
-
-    foo_file = models.FileField(upload_to='foo_uploads/')
diff --git a/tests/integration/settings.py b/tests/integration/settings.py
deleted file mode 100644
--- a/tests/integration/settings.py
+++ /dev/null
@@ -1,28 +0,0 @@
-AZURE_EMULATED_MODE = True
-AZURE_ACCOUNT_NAME = "XXX"
-AZURE_ACCOUNT_KEY = "KXXX"
-AZURE_CONTAINER = "test"
-
-SECRET_KEY = 'test'
-
-INSTALLED_APPS = (
-    'django.contrib.staticfiles',
-    'storages',
-    'tests.integration'
-)
-
-TEMPLATES = [
-    {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [],
-        'APP_DIRS': True,
-        'OPTIONS': {},
-    },
-]
-
-DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': ':memory:'
-    }
-}
diff --git a/tests/integration/test_azure.py b/tests/integration/test_azure.py
deleted file mode 100644
--- a/tests/integration/test_azure.py
+++ /dev/null
@@ -1,306 +0,0 @@
-import io
-
-from django import forms
-from django.core.files.storage import default_storage
-from django.core.files.uploadedfile import SimpleUploadedFile
-from django.template import Context, Template
-from django.test import TestCase, override_settings
-from django.utils import timezone
-
-from storages.backends import azure_storage
-from tests.integration.models import SimpleFileModel
-
-
-class AzureStorageTest(TestCase):
-
-    def setUp(self, *args):
-        self.storage = azure_storage.AzureStorage()
-        self.storage.is_emulated = True
-        self.storage.account_name = "XXX"
-        self.storage.account_key = "KXXX"
-        self.storage.azure_container = "test"
-        self.storage.service.delete_container(
-            self.storage.azure_container, fail_not_exist=False)
-        self.storage.service.create_container(
-            self.storage.azure_container, public_access=False, fail_on_exist=False)
-
-    def test_save(self):
-        expected_name = "some blob Ϊ.txt"
-        self.assertFalse(self.storage.exists(expected_name))
-        stream = io.BytesIO(b'Im a stream')
-        name = self.storage.save(expected_name, stream)
-        self.assertEqual(name, expected_name)
-        self.assertTrue(self.storage.exists(expected_name))
-
-    def test_delete(self):
-        self.storage.location = 'path'
-        expected_name = "some blob Ϊ.txt"
-        self.assertFalse(self.storage.exists(expected_name))
-        stream = io.BytesIO(b'Im a stream')
-        name = self.storage.save(expected_name, stream)
-        self.assertEqual(name, expected_name)
-        self.assertTrue(self.storage.exists(expected_name))
-        self.storage.delete(expected_name)
-        self.assertFalse(self.storage.exists(expected_name))
-
-    def test_size(self):
-        self.storage.location = 'path'
-        expected_name = "some path/some blob Ϊ.txt"
-        self.assertFalse(self.storage.exists(expected_name))
-        stream = io.BytesIO(b'Im a stream')
-        name = self.storage.save(expected_name, stream)
-        self.assertEqual(name, expected_name)
-        self.assertTrue(self.storage.exists(expected_name))
-        self.assertEqual(self.storage.size(expected_name), len(b'Im a stream'))
-
-    def test_url(self):
-        self.assertTrue(
-            self.storage.url("my_file.txt").endswith("/test/my_file.txt"))
-        self.storage.expiration_secs = 360
-        # has some query-string
-        self.assertTrue("/test/my_file.txt?" in self.storage.url("my_file.txt"))
-
-    def test_url_unsafe_chars(self):
-        name = "my?file <foo>.txt"
-        expected = "/test/my%3Ffile%20%3Cfoo%3E.txt"
-        self.assertTrue(
-            self.storage.url(name).endswith(expected))
-        # has some query-string
-        self.storage.expiration_secs = 360
-        self.assertTrue("{}?".format(expected) in self.storage.url(name))
-
-    def test_url_custom_endpoint(self):
-        storage = azure_storage.AzureStorage()
-        storage.is_emulated = True
-        storage.custom_domain = 'foobar:123456'
-        self.assertTrue(storage.url("my_file.txt").startswith('https://foobar:123456/'))
-
-    @override_settings(USE_TZ=True)
-    def test_get_modified_time_tz(self):
-        stream = io.BytesIO(b'Im a stream')
-        name = self.storage.save('some path/some blob Ϊ.txt', stream)
-        self.assertTrue(timezone.is_aware(self.storage.get_modified_time(name)))
-
-    @override_settings(USE_TZ=False)
-    def test_get_modified_time_no_tz(self):
-        stream = io.BytesIO(b'Im a stream')
-        name = self.storage.save('some path/some blob Ϊ.txt', stream)
-        self.assertTrue(timezone.is_naive(self.storage.get_modified_time(name)))
-
-    @override_settings(USE_TZ=True)
-    def test_modified_time_tz(self):
-        stream = io.BytesIO(b'Im a stream')
-        name = self.storage.save('some path/some blob Ϊ.txt', stream)
-        self.assertTrue(timezone.is_naive(self.storage.modified_time(name)))
-
-    @override_settings(USE_TZ=False)
-    def test_modified_time_no_tz(self):
-        stream = io.BytesIO(b'Im a stream')
-        name = self.storage.save('some path/some blob Ϊ.txt', stream)
-        self.assertTrue(timezone.is_naive(self.storage.modified_time(name)))
-
-    def test_open_read(self):
-        self.storage.location = 'root'
-        stream = io.BytesIO(b'Im a stream')
-        name = self.storage.save('path/some file.txt', stream)
-        fh = self.storage.open(name, 'r+b')
-        try:
-            self.assertEqual(fh.read(), b'Im a stream')
-        finally:
-            fh.close()
-
-        stream = io.BytesIO()
-        self.storage.service.get_blob_to_stream(
-            container_name=self.storage.azure_container,
-            blob_name='root/path/some file.txt',
-            stream=stream,
-            max_connections=1,
-            timeout=10)
-        stream.seek(0)
-        self.assertEqual(stream.read(), b'Im a stream')
-
-    def test_open_write(self):
-        self.storage.location = 'root'
-        name = 'file.txt'
-        path = 'root/file.txt'
-        fh = self.storage.open(name, 'wb')
-        try:
-            fh.write(b'foo')
-        finally:
-            fh.close()
-
-        stream = io.BytesIO()
-        self.storage.service.get_blob_to_stream(
-            container_name=self.storage.azure_container,
-            blob_name=path,
-            stream=stream,
-            max_connections=1,
-            timeout=10)
-        stream.seek(0)
-        self.assertEqual(stream.read(), b'foo')
-
-        # Write again
-
-        fh = self.storage.open(name, 'wb')
-        try:
-            fh.write(b'bar')
-        finally:
-            fh.close()
-
-        stream = io.BytesIO()
-        self.storage.service.get_blob_to_stream(
-            container_name=self.storage.azure_container,
-            blob_name=path,
-            stream=stream,
-            max_connections=1,
-            timeout=10)
-        stream.seek(0)
-        self.assertEqual(stream.read(), b'bar')
-
-    def test_open_read_write(self):
-        self.storage.location = 'root'
-        stream = io.BytesIO(b'Im a stream')
-        name = self.storage.save('file.txt', stream)
-        fh = self.storage.open(name, 'r+b')
-        try:
-            self.assertEqual(fh.read(), b'Im a stream')
-            fh.write(b' foo')
-            fh.seek(0)
-            self.assertEqual(fh.read(), b'Im a stream foo')
-        finally:
-            fh.close()
-
-        stream = io.BytesIO()
-        self.storage.service.get_blob_to_stream(
-            container_name=self.storage.azure_container,
-            blob_name='root/file.txt',
-            stream=stream,
-            max_connections=1,
-            timeout=10)
-        stream.seek(0)
-        self.assertEqual(stream.read(), b'Im a stream foo')
-
-
-class AzureStorageExpiry(azure_storage.AzureStorage):
-
-    account_name = 'myaccount'
-    account_key = 'mykey'
-    azure_container = 'test_private'
-    expiration_secs = 360
-
-
-class AzureStorageSpecialChars(azure_storage.AzureStorage):
-    def get_valid_name(self, name):
-        return name
-
-
-class FooFileForm(forms.Form):
-
-    foo_file = forms.FileField()
-
-
-class FooFileModelForm(forms.ModelForm):
-    class Meta:
-        model = SimpleFileModel
-        fields = ['foo_file']
-
-
-@override_settings(
-    DEFAULT_FILE_STORAGE='storages.backends.azure_storage.AzureStorage',
-    STATICFILES_STORAGE='storages.backends.azure_storage.AzureStorage')
-class AzureStorageDjangoTest(TestCase):
-
-    def setUp(self, *args):
-        default_storage.service.delete_container(
-            default_storage.azure_container, fail_not_exist=False)
-        default_storage.service.create_container(
-            default_storage.azure_container, public_access=False, fail_on_exist=False)
-
-    def test_is_azure(self):
-        self.assertIsInstance(default_storage, azure_storage.AzureStorage)
-
-    def test_template_static_file(self):
-        t = Template(
-            '{% load static from staticfiles %}'
-            '{% static "foo.txt" %}')
-        self.assertEqual(
-            t.render(Context({})).strip(),
-            "https://127.0.0.1:10000/devstoreaccount1/test/foo.txt")
-
-    @override_settings(
-        DEFAULT_FILE_STORAGE='tests.integration.test_azure.AzureStorageExpiry')
-    def test_template_media_file(self):
-        t = Template('{{ file_url }}')
-        rendered = t.render(Context({
-            'file_url': default_storage.url('foo.txt')})).strip()
-        self.assertTrue(
-            "https://127.0.0.1:10000/devstoreaccount1/test_private/foo.txt?" in rendered)
-        self.assertTrue("&amp;" in rendered)
-
-        # check static files still work
-        t = Template(
-            '{% load static from staticfiles %}'
-            '{% static "foo.txt" %}')
-        self.assertEqual(
-            t.render(Context({})).strip(),
-            "https://127.0.0.1:10000/devstoreaccount1/test/foo.txt")
-
-    def test_form(self):
-        files = {
-            'foo_file': SimpleUploadedFile(
-                name='1234.pdf',
-                content=b'foo content',
-                content_type='application/pdf')}
-        form = FooFileForm(data={}, files=files)
-        self.assertTrue(form.is_valid())
-        name = default_storage.save(
-            'foo.pdf', form.cleaned_data['foo_file'])
-        self.assertEqual(name, 'foo.pdf')
-        self.assertTrue(default_storage.exists('foo.pdf'))
-
-    def test_model_form(self):
-        files = {
-            'foo_file': SimpleUploadedFile(
-                name='foo.pdf',
-                content=b'foo content',
-                content_type='application/pdf')}
-        form = FooFileModelForm(data={}, files=files)
-        self.assertTrue(form.is_valid())
-        form.save()
-        self.assertTrue(default_storage.exists('foo_uploads/foo.pdf'))
-
-        # check file content was saved
-        fh = default_storage.open('foo_uploads/foo.pdf', 'r+b')
-        try:
-            self.assertEqual(fh.read(), b'foo content')
-        finally:
-            fh.close()
-
-    def test_name_clean_issue_609(self):
-        """
-        Should strip special characters when using the default storage
-        """
-        simple_file = SimpleFileModel()
-        simple_file.foo_file = SimpleUploadedFile(
-            name='foo%?:;~bar.txt',
-            content=b'foo content')
-        simple_file.save()
-        self.assertEqual(simple_file.foo_file.name, 'foo_uploads/foobar.txt')
-        self.assertTrue('foobar.txt' in simple_file.foo_file.url)
-
-    @override_settings(
-        DEFAULT_FILE_STORAGE='tests.integration.test_azure.AzureStorageSpecialChars')
-    def test_name_clean_issue_609_with_special_chars(self):
-        """
-        Should not strip special chars
-        """
-        name = 'foo%?:;~bar.txt'
-        simple_file = SimpleFileModel()
-        simple_file.foo_file = SimpleUploadedFile(
-            name=name,
-            content=b'foo content')
-        simple_file.save()
-        self.assertEqual(
-            simple_file.foo_file.name, 'foo_uploads/{}'.format(name))
-        self.assertTrue(
-            'foo_uploads/foo%25%3F%3A%3B~bar.txt' in simple_file.foo_file.url)
diff --git a/tests/test_azure.py b/tests/test_azure.py
--- a/tests/test_azure.py
+++ b/tests/test_azure.py
@@ -4,7 +4,8 @@
 
 import django
 import pytz
-from azure.storage.blob import Blob, BlobPermissions, BlobProperties
+from azure.core.exceptions import ResourceNotFoundError
+from azure.storage.blob import BlobProperties
 from django.core.exceptions import SuspiciousOperation
 from django.core.files.base import ContentFile
 from django.test import TestCase, override_settings
@@ -16,10 +17,13 @@ class AzureStorageTest(TestCase):
 
     def setUp(self, *args):
         self.storage = azure_storage.AzureStorage()
-        self.storage._service = mock.MagicMock()
-        self.storage._custom_service = mock.MagicMock()
+        self.storage._client = mock.MagicMock()
         self.storage.overwrite_files = True
+        self.account_name = 'test'
+        self.account_key = 'key'
         self.container_name = 'test'
+        self.storage.azure_account = self.account_name
+        self.storage.account_key = self.account_key
         self.storage.azure_container = self.container_name
 
     def test_get_valid_path(self):
@@ -81,36 +85,42 @@ def test_get_valid_path_idempotency(self):
 
     def test_get_available_name(self):
         self.storage.overwrite_files = False
-        self.storage._service.exists.side_effect = [True, False]
+        client_mock = mock.MagicMock()
+        client_mock.get_blob_properties.side_effect = [True, ResourceNotFoundError]
+        self.storage._client.get_blob_client.return_value = client_mock
         name = self.storage.get_available_name('foo.txt')
         self.assertTrue(name.startswith('foo_'))
         self.assertTrue(name.endswith('.txt'))
         self.assertTrue(len(name) > len('foo.txt'))
-        self.assertEqual(self.storage._service.exists.call_count, 2)
+        self.assertEqual(client_mock.get_blob_properties.call_count, 2)
 
     def test_get_available_name_first(self):
         self.storage.overwrite_files = False
-        self.storage._service.exists.return_value = False
+        client_mock = mock.MagicMock()
+        client_mock.get_blob_properties.side_effect = [ResourceNotFoundError]
+        self.storage._client.get_blob_client.return_value = client_mock
         self.assertEqual(
             self.storage.get_available_name('foo bar baz.txt'),
             'foo bar baz.txt')
-        self.assertEqual(self.storage._service.exists.call_count, 1)
+        self.assertEqual(client_mock.get_blob_properties.call_count, 1)
 
     def test_get_available_name_max_len(self):
         self.storage.overwrite_files = False
         # if you wonder why this is, file-system
         # storage will raise when file name is too long as well,
         # the form should validate this
+        client_mock = mock.MagicMock()
+        client_mock.get_blob_properties.side_effect = [False, ResourceNotFoundError]
+        self.storage._client.get_blob_client.return_value = client_mock
         self.assertRaises(ValueError, self.storage.get_available_name, 'a' * 1025)
-        self.storage._service.exists.side_effect = [True, False]
         name = self.storage.get_available_name('a' * 1000, max_length=100)  # max_len == 1024
         self.assertEqual(len(name), 100)
         self.assertTrue('_' in name)
-        self.assertEqual(self.storage._service.exists.call_count, 2)
+        self.assertEqual(client_mock.get_blob_properties.call_count, 2)
 
     def test_get_available_invalid(self):
         self.storage.overwrite_files = False
-        self.storage._service.exists.return_value = False
+        self.storage._client.get_blob_properties.return_value = False
         if django.VERSION[:2] == (3, 0):
             # Django 2.2.21 added this security fix:
             # https://docs.djangoproject.com/en/3.2/releases/2.2.21/#cve-2021-31542-potential-directory-traversal-via-uploaded-files
@@ -128,81 +138,58 @@ def test_get_available_invalid(self):
         self.assertRaises(ValueError, self.storage.get_available_name, "...")
 
     def test_url(self):
-        self.storage._custom_service.make_blob_url.return_value = 'ret_foo'
-        self.assertEqual(self.storage.url('some blob'), 'ret_foo')
-        self.storage._custom_service.make_blob_url.assert_called_once_with(
-            container_name=self.container_name,
-            blob_name='some%20blob',
-            protocol='https')
+        blob_mock = mock.MagicMock()
+        blob_mock.url = 'https://ret_foo.blob.core.windows.net/test/some%20blob'
+        self.storage._client.get_blob_client.return_value = blob_mock
+        self.assertEqual(self.storage.url('some blob'), blob_mock.url)
+        self.storage._client.get_blob_client.assert_called_once_with('some blob')
 
     def test_url_unsafe_chars(self):
-        self.storage.custom_service.make_blob_url.return_value = 'ret_foo'
-        self.assertEqual(self.storage.url('foo;?:@=&"<>#%{}|^~[]`bar/~!*()\''), 'ret_foo')
-        self.storage.custom_service.make_blob_url.assert_called_once_with(
-            container_name=self.container_name,
-            blob_name='foo%3B%3F%3A%40%3D%26%22%3C%3E%23%25%7B%7D%7C%5E~%5B%5D%60bar/~!*()\'',
-            protocol='https')
-
-    def test_url_expire(self):
+        blob_mock = mock.MagicMock()
+        blob_mock.url = 'https://ret_foo.blob.core.windows.net/test/some%20blob'
+        self.storage._client.get_blob_client.return_value = blob_mock
+        self.assertEqual(
+            self.storage.url('foo;?:@=&"<>#%{}|^~[]`bar/~!*()\''), blob_mock.url)
+        self.storage.client.get_blob_client.assert_called_once_with(
+            'foo;?:@=&"<>#%{}|^~[]`bar/~!*()\'')
+
+    @mock.patch('storages.backends.azure_storage.generate_blob_sas')
+    def test_url_expire(self, generate_blob_sas_mocked):
+        generate_blob_sas_mocked.return_value = 'foo_token'
+        blob_mock = mock.MagicMock()
+        blob_mock.url = 'https://ret_foo.blob.core.windows.net/test/some%20blob'
+        self.storage._client.get_blob_client.return_value = blob_mock
+        self.storage.account_name = self.account_name
+
         utc = pytz.timezone('UTC')
         fixed_time = utc.localize(datetime.datetime(2016, 11, 6, 4))
-        self.storage._custom_service.generate_blob_shared_access_signature.return_value = 'foo_token'
-        self.storage._custom_service.make_blob_url.return_value = 'ret_foo'
         with mock.patch('storages.backends.azure_storage.datetime') as d_mocked:
             d_mocked.utcnow.return_value = fixed_time
-            self.assertEqual(self.storage.url('some blob', 100), 'ret_foo')
-            self.storage._custom_service.generate_blob_shared_access_signature.assert_called_once_with(
+            self.assertEquals(
+                self.storage.url('some blob', 100),
+                'https://ret_foo.blob.core.windows.net/test/some%20blob')
+            generate_blob_sas_mocked.assert_called_once_with(
+                self.account_name,
                 self.container_name,
                 'some blob',
-                permission=BlobPermissions.READ,
+                account_key=self.account_key,
+                permission=mock.ANY,
                 expiry=fixed_time + timedelta(seconds=100))
-            self.storage._custom_service.make_blob_url.assert_called_once_with(
-                container_name=self.container_name,
-                blob_name='some%20blob',
-                sas_token='foo_token',
-                protocol='https')
 
-    def test_blob_service_default_params(self):
+    def test_container_client_default_params(self):
         storage = azure_storage.AzureStorage()
+        storage.account_name = self.account_name
         with mock.patch(
-                'storages.backends.azure_storage.BlockBlobService',
-                autospec=True) as c_mocked:
-            self.assertIsNotNone(storage.service)
-            c_mocked.assert_called_once_with(
-                account_name=None,
-                account_key=None,
-                sas_token=None,
-                is_emulated=False,
-                protocol='https',
-                custom_domain=None,
-                connection_string=None,
-                token_credential=None,
-                endpoint_suffix=None)
-
-    def test_blob_service_params_no_emulator(self):
-        """Should ignore custom domain when emulator is not used"""
-        storage = azure_storage.AzureStorage()
-        storage.is_emulated = False
-        storage.custom_domain = 'foo_domain'
-        with mock.patch(
-                'storages.backends.azure_storage.BlockBlobService',
+                'storages.backends.azure_storage.ContainerClient',
                 autospec=True) as c_mocked:
-            self.assertIsNotNone(storage.service)
+            self.assertIsNotNone(storage.client)
             c_mocked.assert_called_once_with(
-                account_name=None,
-                account_key=None,
-                sas_token=None,
-                is_emulated=False,
-                protocol='https',
-                custom_domain=None,
-                connection_string=None,
-                token_credential=None,
-                endpoint_suffix=None)
-
-    def test_blob_service_params(self):
+                'https://test.blob.core.windows.net',
+                None,
+                credential=None)
+
+    def test_container_client_params(self):
         storage = azure_storage.AzureStorage()
-        storage.is_emulated = True
-        storage.endpoint_suffix = 'foo_suffix'
         storage.account_name = 'foo_name'
         storage.account_key = 'foo_key'
         storage.sas_token = 'foo_token'
@@ -211,82 +198,13 @@ def test_blob_service_params(self):
         storage.connection_string = 'foo_conn'
         storage.token_credential = 'foo_cred'
         with mock.patch(
-                'storages.backends.azure_storage.BlockBlobService',
-                autospec=True) as c_mocked:
-            self.assertIsNotNone(storage.service)
-            c_mocked.assert_called_once_with(
-                account_name='foo_name',
-                account_key='foo_key',
-                sas_token='foo_token',
-                is_emulated=True,
-                protocol='https',
-                custom_domain='foo_domain',
-                connection_string='foo_conn',
-                token_credential='foo_cred',
-                endpoint_suffix='foo_suffix')
-
-    def test_blob_custom_service_default_params(self):
-        storage = azure_storage.AzureStorage()
-        with mock.patch(
-                'storages.backends.azure_storage.BlockBlobService',
-                autospec=True) as c_mocked:
-            self.assertIsNotNone(storage.custom_service)
-            c_mocked.assert_called_once_with(
-                account_name=None,
-                account_key=None,
-                sas_token=None,
-                is_emulated=False,
-                protocol='https',
-                custom_domain=None,
-                connection_string=None,
-                token_credential=None,
-                endpoint_suffix=None)
-
-    def test_blob_custom_service_params_no_emulator(self):
-        """Should pass custom domain when emulator is not used"""
-        storage = azure_storage.AzureStorage()
-        storage.is_emulated = False
-        storage.custom_domain = 'foo_domain'
-        with mock.patch(
-                'storages.backends.azure_storage.BlockBlobService',
-                autospec=True) as c_mocked:
-            self.assertIsNotNone(storage.custom_service)
-            c_mocked.assert_called_once_with(
-                account_name=None,
-                account_key=None,
-                sas_token=None,
-                is_emulated=False,
-                protocol='https',
-                custom_domain='foo_domain',
-                connection_string=None,
-                token_credential=None,
-                endpoint_suffix=None)
-
-    def test_blob_custom_service_params(self):
-        storage = azure_storage.AzureStorage()
-        storage.is_emulated = True
-        storage.endpoint_suffix = 'foo_suffix'
-        storage.account_name = 'foo_name'
-        storage.account_key = 'foo_key'
-        storage.sas_token = 'foo_token'
-        storage.azure_ssl = True
-        storage.custom_domain = 'foo_domain'
-        storage.custom_connection_string = 'foo_conn'
-        storage.token_credential = 'foo_cred'
-        with mock.patch(
-                'storages.backends.azure_storage.BlockBlobService',
+                'storages.backends.azure_storage.ContainerClient',
                 autospec=True) as c_mocked:
-            self.assertIsNotNone(storage.custom_service)
+            self.assertIsNotNone(storage.client)
             c_mocked.assert_called_once_with(
-                account_name='foo_name',
-                account_key='foo_key',
-                sas_token='foo_token',
-                is_emulated=True,
-                protocol='https',
-                custom_domain='foo_domain',
-                connection_string='foo_conn',
-                token_credential='foo_cred',
-                endpoint_suffix='foo_suffix')
+                'foo_conn',
+                None,
+                credential='foo_key')
 
     # From boto3
 
@@ -299,13 +217,13 @@ def test_storage_save(self):
         with mock.patch('storages.backends.azure_storage.ContentSettings') as c_mocked:
             c_mocked.return_value = 'content_settings_foo'
             self.assertEqual(self.storage.save(name, content), name)
-            self.storage._service.create_blob_from_stream.assert_called_once_with(
-                container_name=self.container_name,
-                blob_name=name,
-                stream=content.file,
+            self.storage._client.upload_blob.assert_called_once_with(
+                name,
+                content.file,
                 content_settings='content_settings_foo',
-                max_connections=2,
-                timeout=20)
+                max_concurrency=2,
+                timeout=20,
+                overwrite=True)
             c_mocked.assert_called_once_with(
                 content_type='text/plain',
                 content_encoding=None,
@@ -322,27 +240,25 @@ def test_storage_open_write(self):
         file.write(content)
         written_file = file.file
         file.close()
-        self.storage._service.create_blob_from_stream.assert_called_once_with(
-            container_name=self.container_name,
-            blob_name=name,
-            stream=written_file,
+        self.storage._client.upload_blob.assert_called_once_with(
+            name,
+            written_file,
             content_settings=mock.ANY,
-            max_connections=2,
-            timeout=20)
+            max_concurrency=2,
+            timeout=20,
+            overwrite=True)
 
     def test_storage_exists(self):
-        self.storage._service.exists.return_value = True
         blob_name = "blob"
+        client_mock = mock.MagicMock()
+        self.storage._client.get_blob_client.return_value = client_mock
         self.assertTrue(self.storage.exists(blob_name))
-        self.storage._service.exists.assert_called_once_with(
-            self.container_name, blob_name, timeout=20)
+        self.assertEqual(client_mock.get_blob_properties.call_count, 1)
 
     def test_delete_blob(self):
         self.storage.delete("name")
-        self.storage._service.delete_blob.assert_called_once_with(
-            container_name=self.container_name,
-            blob_name="name",
-            timeout=20)
+        self.storage._client.delete_blob.assert_called_once_with(
+            "name", timeout=20)
 
     def test_storage_listdir_base(self):
         file_names = ["some/path/1.txt", "2.txt", "other/path/3.txt", "4.txt"]
@@ -352,11 +268,11 @@ def test_storage_listdir_base(self):
             obj = mock.MagicMock()
             obj.name = p
             result.append(obj)
-        self.storage._service.list_blobs.return_value = iter(result)
+        self.storage._client.list_blobs.return_value = iter(result)
 
         dirs, files = self.storage.listdir("")
-        self.storage._service.list_blobs.assert_called_with(
-            self.container_name, prefix="", timeout=20)
+        self.storage._client.list_blobs.assert_called_with(
+            name_starts_with="", timeout=20)
 
         self.assertEqual(len(dirs), 2)
         for directory in ["some", "other"]:
@@ -378,11 +294,11 @@ def test_storage_listdir_subdir(self):
             obj = mock.MagicMock()
             obj.name = p
             result.append(obj)
-        self.storage._service.list_blobs.return_value = iter(result)
+        self.storage._client.list_blobs.return_value = iter(result)
 
         dirs, files = self.storage.listdir("some/")
-        self.storage._service.list_blobs.assert_called_with(
-            self.container_name, prefix="some/", timeout=20)
+        self.storage._client.list_blobs.assert_called_with(
+            name_starts_with="some/", timeout=20)
 
         self.assertEqual(len(dirs), 1)
         self.assertTrue(
@@ -396,15 +312,17 @@ def test_storage_listdir_subdir(self):
 
     def test_size_of_file(self):
         props = BlobProperties()
-        props.content_length = 12
-        self.storage._service.get_blob_properties.return_value = Blob(props=props)
+        props.size = 12
+        client_mock = mock.MagicMock()
+        client_mock.get_blob_properties.return_value = props
+        self.storage._client.get_blob_client.return_value = client_mock
         self.assertEqual(12, self.storage.size("name"))
 
     def test_last_modified_of_file(self):
         props = BlobProperties()
         accepted_time = datetime.datetime(2017, 5, 11, 8, 52, 4)
         props.last_modified = accepted_time
-        self.storage._service.get_blob_properties.return_value = Blob(props=props)
+        self.storage._client.get_blob_properties.return_value = props
         time = self.storage.modified_time("name")
         self.assertEqual(accepted_time, time)