diff --git a/requirements-dev.txt b/requirements-dev.txt index a03b408..0ee5db6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,17 +1,20 @@ -r requirements.txt -pytest -ipdb -flake8 -pep8-naming -autoflake -mccabe -yapf -tox -coverage -pytest-cov -zest.releaser -setuptools -twine -wheel -rope -pytest-mock +pytest==5.4.3 +ipdb==0.13.2 +flake8==3.8.3 +pep8-naming==0.11.1 +autoflake==1.3.1 +mccabe==0.6.1 +yapf==0.30.0 +tox==3.15.2 +coverage==5.1 +pytest-cov==2.10.0 +zest.releaser==6.20.1 +setuptools==47.3.1 +twine==3.1.1 +wheel==0.34.2 +rope==0.17.0 +pytest-mock==3.1.1 +brunette==0.1.5 +lock-requirements==0.1.1 +requests-mock diff --git a/src/mega/errors.py b/src/mega/errors.py index 4df7f90..218a935 100644 --- a/src/mega/errors.py +++ b/src/mega/errors.py @@ -6,17 +6,16 @@ class ValidationError(Exception): _CODE_TO_DESCRIPTIONS = { + 0: ('UNKNOWN', 'API Returned 0'), -1: ( - 'EINTERNAL', - ( + 'EINTERNAL', ( 'An internal error has occurred. Please submit a bug report, ' 'detailing the exact circumstances in which this error occurred' ) ), -2: ('EARGS', 'You have passed invalid arguments to this command'), -3: ( - 'EAGAIN', - ( + 'EAGAIN', ( '(always at the request level) A temporary congestion or server ' 'malfunction prevented your request from being processed. ' 'No data was altered. Retry. Retries must be spaced with ' @@ -24,8 +23,7 @@ _CODE_TO_DESCRIPTIONS = { ) ), -4: ( - 'ERATELIMIT', - ( + 'ERATELIMIT', ( 'You have exceeded your command weight per time quota. Please ' 'wait a few seconds, then try again (this should never happen ' 'in sane real-life applications)' @@ -37,15 +35,13 @@ _CODE_TO_DESCRIPTIONS = { 'Too many concurrent IP addresses are accessing this upload target URL' ), -7: ( - 'ERANGE', - ( + 'ERANGE', ( 'The upload file packet is out of range or not starting and ' 'ending on a chunk boundary' ) ), -8: ( - 'EEXPIRED', - ( + 'EEXPIRED', ( 'The upload target URL you are trying to access has expired. ' 'Please request a fresh one' ) diff --git a/src/mega/mega.py b/src/mega/mega.py index 83dc983..092962a 100644 --- a/src/mega/mega.py +++ b/src/mega/mega.py @@ -174,24 +174,41 @@ class Mega: data = [data] url = f'{self.schema}://g.api.{self.domain}/cs' - req = requests.post( + response = requests.post( url, params=params, data=json.dumps(data), timeout=self.timeout, ) - json_resp = json.loads(req.text) - if isinstance(json_resp, int): - if json_resp == -3: + json_resp = json.loads(response.text) + try: + if isinstance(json_resp, list): + int_resp = json_resp[0] if isinstance( + json_resp[0], int + ) else None + elif isinstance(json_resp, int): + int_resp = json_resp + except IndexError: + int_resp = None + if int_resp is not None: + if int_resp == -3: msg = 'Request failed, retrying' logger.info(msg) raise RuntimeError(msg) - raise RequestError(json_resp) + raise RequestError(int_resp) return json_resp[0] def _parse_url(self, url): - # parse file id and key from url - if '!' in url: + """Parse file id and key from url.""" + if '/file/' in url: + # V2 URL structure + url = url.replace(' ', '') + file_id = re.findall(r'\W\w\w\w\w\w\w\w\w\W', url)[0][1:-1] + id_index = re.search(file_id, url).end() + key = url[id_index + 1:] + return f'{file_id}!{key}' + elif '!' in url: + # V1 URL structure match = re.findall(r'/#!(.*)', url) path = match[0] return path @@ -874,9 +891,12 @@ class Mega: # update attributes data = self._api_request( { - 'a': 'p', - 't': dest, - 'i': self.request_id, + 'a': + 'p', + 't': + dest, + 'i': + self.request_id, 'n': [ { 'h': completion_file_handle, @@ -902,8 +922,10 @@ class Mega: # update attributes data = self._api_request( { - 'a': 'p', - 't': parent_node_id, + 'a': + 'p', + 't': + parent_node_id, 'n': [ { 'h': 'xxxxxxxx', @@ -912,7 +934,8 @@ class Mega: 'k': encrypted_key } ], - 'i': self.request_id + 'i': + self.request_id } ) return data @@ -1102,8 +1125,10 @@ class Mega: encrypted_name = base64_url_encode(encrypt_attr({'n': dest_name}, k)) return self._api_request( { - 'a': 'p', - 't': dest_node['h'], + 'a': + 'p', + 't': + dest_node['h'], 'n': [ { 'ph': file_handle, diff --git a/src/tests/test_mega.py b/src/tests/test_mega.py index 48ed6e7..5c36e15 100644 --- a/src/tests/test_mega.py +++ b/src/tests/test_mega.py @@ -2,6 +2,7 @@ import random from pathlib import Path import os +import requests_mock import pytest from mega import Mega @@ -11,6 +12,7 @@ TEST_PUBLIC_URL = ( 'https://mega.nz/#!hYVmXKqL!r0d0-WRnFwulR_shhuEDwrY1Vo103-am1MyUy8oV6Ps' ) TEST_FILE = os.path.basename(__file__) +MODULE = 'mega.mega' @pytest.fixture @@ -32,9 +34,7 @@ def mega(folder_name): def uploaded_file(mega, folder_name): folder = mega.find(folder_name) dest_node_id = folder[1]['h'] - mega.upload( - __file__, dest=dest_node_id, dest_filename='test.py' - ) + mega.upload(__file__, dest=dest_node_id, dest_filename='test.py') path = f'{folder_name}/test.py' return mega.find(path) @@ -73,7 +73,6 @@ def test_get_link(mega, uploaded_file): class TestExport: - def test_export_folder(self, mega, folder_name): public_url = None for _ in range(2): @@ -103,9 +102,7 @@ class TestExport: # Upload a single file into a folder folder = mega.find(folder_name) dest_node_id = folder[1]['h'] - mega.upload( - __file__, dest=dest_node_id, dest_filename='test.py' - ) + mega.upload(__file__, dest=dest_node_id, dest_filename='test.py') path = f'{folder_name}/test.py' assert mega.find(path) @@ -143,13 +140,10 @@ class TestCreateFolder: class TestFind: - def test_find_file(self, mega, folder_name): folder = mega.find(folder_name) dest_node_id = folder[1]['h'] - mega.upload( - __file__, dest=dest_node_id, dest_filename='test.py' - ) + mega.upload(__file__, dest=dest_node_id, dest_filename='test.py') path = f'{folder_name}/test.py' assert mega.find(path) @@ -194,9 +188,7 @@ def test_download(mega, tmpdir, folder_name): # Upload a single file into a folder folder = mega.find(folder_name) dest_node_id = folder[1]['h'] - mega.upload( - __file__, dest=dest_node_id, dest_filename='test.py' - ) + mega.upload(__file__, dest=dest_node_id, dest_filename='test.py') path = f'{folder_name}/test.py' file = mega.find(path) @@ -222,3 +214,33 @@ def test_add_contact(mega): def test_remove_contact(mega): resp = mega.remove_contact(TEST_CONTACT) assert isinstance(resp, int) + + +@pytest.mark.parametrize( + 'url, expected_file_id_and_key', [ + ( + 'https://mega.nz/#!Ue5VRSIQ!kC2E4a4JwfWWCWYNJovGFHlbz8F' + 'N-ISsBAGPzvTjT6k', + 'Ue5VRSIQ!kC2E4a4JwfWWCWYNJovGFHlbz8FN-ISsBAGPzvTjT6k' + ), + ( + 'https://mega.nz/file/cH51DYDR#qH7QOfRcM-7N9riZWdSjsRq' + '5VDTLfIhThx1capgVA30', + 'cH51DYDR!qH7QOfRcM-7N9riZWdSjsRq5VDTLfIhThx1capgVA30' + ), + ] +) +def test_parse_url(url, expected_file_id_and_key, mega): + assert mega._parse_url(url) == expected_file_id_and_key + + +class TestAPIRequest: + @pytest.mark.parametrize('response_text', ['-3', '-9']) + def test_when_api_returns_int_raises_exception( + self, mega, response_text, + ): + with requests_mock.Mocker() as m: + m.post( + f'{mega.schema}://g.api.{mega.domain}/cs', text=response_text + ) + mega._api_request(data={})