Rework Python 2-3 compat (#268)

This commit is contained in:
Quentame 2020-04-08 00:19:42 +02:00 committed by GitHub
parent e3bdcea15a
commit 696db8cf20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 169 additions and 127 deletions

View file

@ -1,14 +1,14 @@
"""Library base file.""" """Library base file."""
import six from six import PY2, string_types
import uuid from uuid import uuid1
import inspect import inspect
import json import json
import logging import logging
from requests import Session from requests import Session
import sys from tempfile import gettempdir
import tempfile from os import path, mkdir
import os
from re import match from re import match
import http.cookiejar as cookielib
from pyicloud.exceptions import ( from pyicloud.exceptions import (
PyiCloudFailedLoginException, PyiCloudFailedLoginException,
@ -27,11 +27,6 @@ from pyicloud.services import (
) )
from pyicloud.utils import get_password_from_keyring from pyicloud.utils import get_password_from_keyring
if six.PY3:
import http.cookiejar as cookielib
else:
import cookielib # pylint: disable=import-error
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -99,7 +94,7 @@ class PyiCloudSession(Session):
reason = data.get("errorMessage") reason = data.get("errorMessage")
reason = reason or data.get("reason") reason = reason or data.get("reason")
reason = reason or data.get("errorReason") reason = reason or data.get("errorReason")
if not reason and isinstance(data.get("error"), six.string_types): if not reason and isinstance(data.get("error"), string_types):
reason = data.get("error") reason = data.get("error")
if not reason and data.get("error"): if not reason and data.get("error"):
reason = "Unknown reason" reason = "Unknown reason"
@ -166,7 +161,7 @@ class PyiCloudService(object):
password = get_password_from_keyring(apple_id) password = get_password_from_keyring(apple_id)
self.data = {} self.data = {}
self.client_id = client_id or str(uuid.uuid1()).upper() self.client_id = client_id or str(uuid1()).upper()
self.with_family = with_family self.with_family = with_family
self.user = {"apple_id": apple_id, "password": password} self.user = {"apple_id": apple_id, "password": password}
@ -176,11 +171,9 @@ class PyiCloudService(object):
self._base_login_url = "%s/login" % self.SETUP_ENDPOINT self._base_login_url = "%s/login" % self.SETUP_ENDPOINT
if cookie_directory: if cookie_directory:
self._cookie_directory = os.path.expanduser( self._cookie_directory = path.expanduser(path.normpath(cookie_directory))
os.path.normpath(cookie_directory)
)
else: else:
self._cookie_directory = os.path.join(tempfile.gettempdir(), "pyicloud",) self._cookie_directory = path.join(gettempdir(), "pyicloud")
self.session = PyiCloudSession(self) self.session = PyiCloudSession(self)
self.session.verify = verify self.session.verify = verify
@ -194,7 +187,7 @@ class PyiCloudService(object):
cookiejar_path = self._get_cookiejar_path() cookiejar_path = self._get_cookiejar_path()
self.session.cookies = cookielib.LWPCookieJar(filename=cookiejar_path) self.session.cookies = cookielib.LWPCookieJar(filename=cookiejar_path)
if os.path.exists(cookiejar_path): if path.exists(cookiejar_path):
try: try:
self.session.cookies.load() self.session.cookies.load()
LOGGER.debug("Read cookies from %s", cookiejar_path) LOGGER.debug("Read cookies from %s", cookiejar_path)
@ -242,8 +235,8 @@ class PyiCloudService(object):
self.params.update({"dsid": self.data["dsInfo"]["dsid"]}) self.params.update({"dsid": self.data["dsInfo"]["dsid"]})
self._webservices = self.data["webservices"] self._webservices = self.data["webservices"]
if not os.path.exists(self._cookie_directory): if not path.exists(self._cookie_directory):
os.mkdir(self._cookie_directory) mkdir(self._cookie_directory)
self.session.cookies.save() self.session.cookies.save()
LOGGER.debug("Cookies saved to %s", self._get_cookiejar_path()) LOGGER.debug("Cookies saved to %s", self._get_cookiejar_path())
@ -252,7 +245,7 @@ class PyiCloudService(object):
def _get_cookiejar_path(self): def _get_cookiejar_path(self):
"""Get path for cookiejar file.""" """Get path for cookiejar file."""
return os.path.join( return path.join(
self._cookie_directory, self._cookie_directory,
"".join([c for c in self.user.get("apple_id") if match(r"\w", c)]), "".join([c for c in self.user.get("apple_id") if match(r"\w", c)]),
) )
@ -373,9 +366,9 @@ class PyiCloudService(object):
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() as_unicode = self.__unicode__()
if sys.version_info[0] >= 3: if PY2:
return as_unicode
return as_unicode.encode("utf-8", "ignore") return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s>" % str(self) return "<%s>" % str(self)

View file

@ -5,10 +5,10 @@ A Command Line Wrapper to allow easy use of pyicloud for
command line scripts, and related. command line scripts, and related.
""" """
from __future__ import print_function from __future__ import print_function
from builtins import input
import argparse import argparse
import pickle import pickle
import sys import sys
import six
from click import confirm from click import confirm
@ -16,12 +16,6 @@ from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloudFailedLoginException from pyicloud.exceptions import PyiCloudFailedLoginException
from . import utils from . import utils
# fmt: off
if six.PY2:
input = raw_input # pylint: disable=redefined-builtin,invalid-name,undefined-variable
else:
input = input # pylint: disable=bad-option-value,self-assigning-variable,invalid-name
# fmt: on
DEVICE_ERROR = "Please use the --device switch to indicate which device to use." DEVICE_ERROR = "Please use the --device switch to indicate which device to use."

View file

@ -1,7 +1,7 @@
"""Account service.""" """Account service."""
import sys from __future__ import division
from six import PY2, python_2_unicode_compatible
import six from collections import OrderedDict
from pyicloud.utils import underscore_to_camelcase from pyicloud.utils import underscore_to_camelcase
@ -68,27 +68,41 @@ class AccountService(object):
return self._storage return self._storage
def __unicode__(self):
return "{devices: %s, family: %s, storage: %s bytes free}" % (
len(self.devices),
len(self.family),
self.storage.usage.available_storage_in_bytes,
)
@six.python_2_unicode_compatible def __str__(self):
as_unicode = self.__unicode__()
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self):
return "<%s: %s>" % (type(self).__name__, str(self))
@python_2_unicode_compatible
class AccountDevice(dict): class AccountDevice(dict):
"""Account device.""" """Account device."""
def __getattr__(self, key): def __getattr__(self, key):
return self[underscore_to_camelcase(key)] return self[underscore_to_camelcase(key)]
def __unicode__(self):
return "{model: %s, name: %s}" % (self.model_display_name, self.name)
def __str__(self): def __str__(self):
return u"{display_name}: {name}".format( as_unicode = self.__unicode__()
display_name=self.model_display_name, name=self.name, if PY2:
) return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<{display}>".format( return "<%s: %s>" % (type(self).__name__, str(self))
display=(
six.text_type(self)
if sys.version_info[0] >= 3
else six.text_type(self).encode("utf8", "replace")
)
)
class FamilyMember(object): class FamilyMember(object):
@ -193,19 +207,20 @@ class FamilyMember(object):
return self._attrs[key] return self._attrs[key]
return getattr(self, key) return getattr(self, key)
def __str__(self): def __unicode__(self):
return u"{full_name}: {age_classification}".format( return "{name: %s, age_classification: %s}" % (
full_name=self.full_name, age_classification=self.age_classification, self.full_name,
self.age_classification,
) )
def __str__(self):
as_unicode = self.__unicode__()
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<{display}>".format( return "<%s: %s>" % (type(self).__name__, str(self))
display=(
six.text_type(self)
if sys.version_info[0] >= 3
else six.text_type(self).encode("utf8", "replace")
)
)
class AccountStorageUsageForMedia(object): class AccountStorageUsageForMedia(object):
@ -234,17 +249,17 @@ class AccountStorageUsageForMedia(object):
"""Gets the usage in bytes.""" """Gets the usage in bytes."""
return self.usage_data["usageInBytes"] return self.usage_data["usageInBytes"]
def __unicode__(self):
return "{key: %s, usage: %s bytes}" % (self.key, self.usage_in_bytes)
def __str__(self): def __str__(self):
return u"{key}: {usage}".format(key=self.key, usage=self.usage_in_bytes) as_unicode = self.__unicode__()
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<{display}>".format( return "<%s: %s>" % (type(self).__name__, str(self))
display=(
six.text_type(self)
if sys.version_info[0] >= 3
else six.text_type(self).encode("utf8", "replace")
)
)
class AccountStorageUsage(object): class AccountStorageUsage(object):
@ -267,7 +282,7 @@ class AccountStorageUsage(object):
@property @property
def used_storage_in_percent(self): def used_storage_in_percent(self):
"""Gets the used storage in percent.""" """Gets the used storage in percent."""
return self.used_storage_in_bytes * 100 / self.total_storage_in_bytes return round(self.used_storage_in_bytes * 100 / self.total_storage_in_bytes, 2)
@property @property
def available_storage_in_bytes(self): def available_storage_in_bytes(self):
@ -277,7 +292,9 @@ class AccountStorageUsage(object):
@property @property
def available_storage_in_percent(self): def available_storage_in_percent(self):
"""Gets the available storage in percent.""" """Gets the available storage in percent."""
return self.available_storage_in_bytes * 100 / self.total_storage_in_bytes return round(
self.available_storage_in_bytes * 100 / self.total_storage_in_bytes, 2
)
@property @property
def total_storage_in_bytes(self): def total_storage_in_bytes(self):
@ -309,19 +326,20 @@ class AccountStorageUsage(object):
"""Gets the paid quota.""" """Gets the paid quota."""
return self.quota_data["paidQuota"] return self.quota_data["paidQuota"]
def __str__(self): def __unicode__(self):
return u"{used_percent}%% used of {total} bytes".format( return "%s%% used of %s bytes" % (
used_percent=self.used_storage_in_percent, total=self.total_storage_in_bytes self.used_storage_in_percent,
self.total_storage_in_bytes,
) )
def __str__(self):
as_unicode = self.__unicode__()
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<{display}>".format( return "<%s: %s>" % (type(self).__name__, str(self))
display=(
six.text_type(self)
if sys.version_info[0] >= 3
else six.text_type(self).encode("utf8", "replace")
)
)
class AccountStorage(object): class AccountStorage(object):
@ -331,9 +349,21 @@ class AccountStorage(object):
self.usage = AccountStorageUsage( self.usage = AccountStorageUsage(
storage_data.get("storageUsageInfo"), storage_data.get("quotaStatus") storage_data.get("storageUsageInfo"), storage_data.get("quotaStatus")
) )
self.usages_by_media = {} self.usages_by_media = OrderedDict()
for usage_media in storage_data.get("storageUsageByMedia"): for usage_media in storage_data.get("storageUsageByMedia"):
self.usages_by_media[usage_media["mediaKey"]] = AccountStorageUsageForMedia( self.usages_by_media[usage_media["mediaKey"]] = AccountStorageUsageForMedia(
usage_media usage_media
) )
def __unicode__(self):
return "{usage: %s, usages_by_media: %s}" % (self.usage, self.usages_by_media)
def __str__(self):
as_unicode = self.__unicode__()
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self):
return "<%s: %s>" % (type(self).__name__, str(self))

View file

@ -17,7 +17,7 @@ class CalendarService(object):
self._service_root = service_root self._service_root = service_root
self._calendar_endpoint = "%s/ca" % self._service_root self._calendar_endpoint = "%s/ca" % self._service_root
self._calendar_refresh_url = "%s/events" % self._calendar_endpoint self._calendar_refresh_url = "%s/events" % self._calendar_endpoint
self._calendar_event_detail_url = "%s/eventdetail" % (self._calendar_endpoint,) self._calendar_event_detail_url = "%s/eventdetail" % self._calendar_endpoint
self._calendars = "%s/startup" % self._calendar_endpoint self._calendars = "%s/startup" % self._calendar_endpoint
self.response = {} self.response = {}

View file

@ -1,8 +1,7 @@
"""Find my iPhone service.""" """Find my iPhone service."""
import json import json
import sys
import six from six import PY2, text_type
from pyicloud.exceptions import PyiCloudNoDevicesException from pyicloud.exceptions import PyiCloudNoDevicesException
@ -70,26 +69,26 @@ class FindMyiPhoneServiceManager(object):
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, int): if isinstance(key, int):
if six.PY3: if PY2:
key = list(self.keys())[key]
else:
key = self.keys()[key] key = self.keys()[key]
else:
key = list(self.keys())[key]
return self._devices[key] return self._devices[key]
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self._devices, attr) return getattr(self._devices, attr)
def __unicode__(self): def __unicode__(self):
return six.text_type(self._devices) return text_type(self._devices)
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() as_unicode = self.__unicode__()
if sys.version_info[0] >= 3: if PY2:
return as_unicode
return as_unicode.encode("utf-8", "ignore") return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return six.text_type(self) return text_type(self)
class AppleDevice(object): class AppleDevice(object):
@ -204,13 +203,13 @@ class AppleDevice(object):
def __unicode__(self): def __unicode__(self):
display_name = self["deviceDisplayName"] display_name = self["deviceDisplayName"]
name = self["name"] name = self["name"]
return "%s: %s" % (display_name, name,) return "%s: %s" % (display_name, name)
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() as_unicode = self.__unicode__()
if sys.version_info[0] >= 3: if PY2:
return as_unicode
return as_unicode.encode("utf-8", "ignore") return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<AppleDevice(%s)>" % str(self) return "<AppleDevice(%s)>" % str(self)

View file

@ -1,14 +1,16 @@
"""Photo service.""" """Photo service."""
import sys
import json import json
import base64 import base64
from six import PY2
# fmt: off
from six.moves.urllib.parse import urlencode # pylint: disable=bad-option-value,relative-import
# fmt: on
from datetime import datetime from datetime import datetime
from pyicloud.exceptions import PyiCloudServiceNotActivatedException from pyicloud.exceptions import PyiCloudServiceNotActivatedException
from pytz import UTC from pytz import UTC
from future.moves.urllib.parse import urlencode
class PhotosService(object): class PhotosService(object):
"""The 'Photos' iCloud service.""" """The 'Photos' iCloud service."""
@ -473,9 +475,9 @@ class PhotoAlbum(object):
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() as_unicode = self.__unicode__()
if sys.version_info[0] >= 3: if PY2:
return as_unicode
return as_unicode.encode("utf-8", "ignore") return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: '%s'>" % (type(self).__name__, self) return "<%s: '%s'>" % (type(self).__name__, self)

View file

@ -1,6 +1,6 @@
"""File service.""" """File service."""
from datetime import datetime from datetime import datetime
import sys from six import PY2
class UbiquityService(object): class UbiquityService(object):
@ -112,9 +112,9 @@ class UbiquityNode(object):
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() as_unicode = self.__unicode__()
if sys.version_info[0] >= 3: if PY2:
return as_unicode
return as_unicode.encode("utf-8", "ignore") return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: '%s'>" % (self.type.capitalize(), self) return "<%s: '%s'>" % (self.type.capitalize(), self)

View file

@ -1,7 +1,7 @@
"""Utils.""" """Utils."""
import getpass import getpass
import keyring import keyring
import sys from sys import stdout
from .exceptions import PyiCloudNoStoredPasswordAvailableException from .exceptions import PyiCloudNoStoredPasswordAvailableException
@ -9,7 +9,7 @@ from .exceptions import PyiCloudNoStoredPasswordAvailableException
KEYRING_SYSTEM = "pyicloud://icloud-password" KEYRING_SYSTEM = "pyicloud://icloud-password"
def get_password(username, interactive=sys.stdout.isatty()): def get_password(username, interactive=stdout.isatty()):
"""Get the password from a username.""" """Get the password from a username."""
try: try:
return get_password_from_keyring(username) return get_password_from_keyring(username)
@ -18,7 +18,7 @@ def get_password(username, interactive=sys.stdout.isatty()):
raise raise
return getpass.getpass( return getpass.getpass(
"Enter iCloud password for {username}: ".format(username=username,) "Enter iCloud password for {username}: ".format(username=username)
) )
@ -40,7 +40,7 @@ def get_password_from_keyring(username):
"No pyicloud password for {username} could be found " "No pyicloud password for {username} could be found "
"in the system keychain. Use the `--store-in-keyring` " "in the system keychain. Use the `--store-in-keyring` "
"command-line option for storing a password for this " "command-line option for storing a password for this "
"username.".format(username=username,) "username.".format(username=username)
) )
return result return result
@ -48,12 +48,12 @@ def get_password_from_keyring(username):
def store_password_in_keyring(username, password): def store_password_in_keyring(username, password):
"""Store the password of a username.""" """Store the password of a username."""
return keyring.set_password(KEYRING_SYSTEM, username, password,) return keyring.set_password(KEYRING_SYSTEM, username, password)
def delete_password_in_keyring(username): def delete_password_in_keyring(username):
"""Delete the password of a username.""" """Delete the password of a username."""
return keyring.delete_password(KEYRING_SYSTEM, username,) return keyring.delete_password(KEYRING_SYSTEM, username)
def underscore_to_camelcase(word, initial_capital=False): def underscore_to_camelcase(word, initial_capital=False):

View file

@ -1,5 +1,7 @@
"""Account service tests.""" """Account service tests."""
from unittest import TestCase from unittest import TestCase
from six import PY3
from . import PyiCloudServiceMock from . import PyiCloudServiceMock
from .const import AUTHENTICATED_USER, VALID_PASSWORD from .const import AUTHENTICATED_USER, VALID_PASSWORD
@ -12,6 +14,12 @@ class AccountServiceTest(TestCase):
def setUp(self): def setUp(self):
self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD).account self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD).account
def test_repr(self):
"""Tests representation."""
# fmt: off
assert repr(self.service) == "<AccountService: {devices: 2, family: 3, storage: 3020076244 bytes free}>"
# fmt: on
def test_devices(self): def test_devices(self):
"""Tests devices.""" """Tests devices."""
assert self.service.devices assert self.service.devices
@ -32,6 +40,10 @@ class AccountServiceTest(TestCase):
assert device["modelSmallPhotoURL2x"] assert device["modelSmallPhotoURL2x"]
assert device["modelSmallPhotoURL1x"] assert device["modelSmallPhotoURL1x"]
assert device["modelDisplayName"] assert device["modelDisplayName"]
# fmt: off
if PY3:
assert repr(device) == "<AccountDevice: {model: "+device.model_display_name+", name: "+device.name+"}>"
# fmt: on
def test_family(self): def test_family(self):
"""Tests family members.""" """Tests family members."""
@ -51,30 +63,39 @@ class AccountServiceTest(TestCase):
assert not member.has_ask_to_buy_enabled assert not member.has_ask_to_buy_enabled
assert not member.share_my_location_enabled_family_members assert not member.share_my_location_enabled_family_members
assert member.dsid_for_purchases assert member.dsid_for_purchases
# fmt: off
assert repr(member) == "<FamilyMember: {name: "+member.full_name+", age_classification: "+member.age_classification+"}>"
# fmt: on
def test_storage(self): def test_storage(self):
"""Tests storage.""" """Tests storage."""
assert self.service.storage assert self.service.storage
# fmt: off
if PY3:
assert repr(self.service.storage) == "<AccountStorage: {usage: 43.75% used of 5368709120 bytes, usages_by_media: OrderedDict([('photos', <AccountStorageUsageForMedia: {key: photos, usage: 0 bytes}>), ('backup', <AccountStorageUsageForMedia: {key: backup, usage: 799008186 bytes}>), ('docs', <AccountStorageUsageForMedia: {key: docs, usage: 449092146 bytes}>), ('mail', <AccountStorageUsageForMedia: {key: mail, usage: 1101522944 bytes}>)])}>"
# fmt: on
def test_storage_usage(self):
"""Tests storage usage."""
assert self.service.storage.usage assert self.service.storage.usage
assert ( usage = self.service.storage.usage
self.service.storage.usage.comp_storage_in_bytes assert usage.comp_storage_in_bytes or usage.comp_storage_in_bytes == 0
or self.service.storage.usage.comp_storage_in_bytes == 0 assert usage.used_storage_in_bytes
) assert usage.used_storage_in_percent
assert self.service.storage.usage.used_storage_in_bytes assert usage.available_storage_in_bytes
assert self.service.storage.usage.used_storage_in_percent assert usage.available_storage_in_percent
assert self.service.storage.usage.available_storage_in_bytes assert usage.total_storage_in_bytes
assert self.service.storage.usage.available_storage_in_percent assert usage.commerce_storage_in_bytes or usage.commerce_storage_in_bytes == 0
assert self.service.storage.usage.total_storage_in_bytes assert not usage.quota_over
assert ( assert not usage.quota_tier_max
self.service.storage.usage.commerce_storage_in_bytes assert not usage.quota_almost_full
or self.service.storage.usage.commerce_storage_in_bytes == 0 assert not usage.quota_paid
) # fmt: off
assert not self.service.storage.usage.quota_over assert repr(usage) == "<AccountStorageUsage: "+str(usage.used_storage_in_percent)+"% used of "+str(usage.total_storage_in_bytes)+" bytes>"
assert not self.service.storage.usage.quota_tier_max # fmt: on
assert not self.service.storage.usage.quota_almost_full
assert not self.service.storage.usage.quota_paid
def test_storage_usages_by_media(self):
"""Tests storage usages by media."""
assert self.service.storage.usages_by_media assert self.service.storage.usages_by_media
for usage_media in self.service.storage.usages_by_media.values(): for usage_media in self.service.storage.usages_by_media.values():
@ -82,3 +103,6 @@ class AccountServiceTest(TestCase):
assert usage_media.label assert usage_media.label
assert usage_media.color assert usage_media.color
assert usage_media.usage_in_bytes or usage_media.usage_in_bytes == 0 assert usage_media.usage_in_bytes or usage_media.usage_in_bytes == 0
# fmt: off
assert repr(usage_media) == "<AccountStorageUsageForMedia: {key: "+usage_media.key+", usage: "+str(usage_media.usage_in_bytes)+" bytes}>"
# fmt: on

View file

@ -5,15 +5,15 @@ from .const import AUTHENTICATED_USER, REQUIRES_2SA_USER, VALID_PASSWORD
from .const_findmyiphone import FMI_FAMILY_WORKING from .const_findmyiphone import FMI_FAMILY_WORKING
import os import os
import sys from six import PY2
import pickle import pickle
import pytest import pytest
from unittest import TestCase from unittest import TestCase
if sys.version_info >= (3, 3): if PY2:
from unittest.mock import patch # pylint: disable=no-name-in-module,import-error
else:
from mock import patch from mock import patch
else:
from unittest.mock import patch # pylint: disable=no-name-in-module,import-error
class TestCmdline(TestCase): class TestCmdline(TestCase):