commit
a698c8a7e9
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -58,3 +58,5 @@ docs/_build/
|
||||||
test_pylast.yaml
|
test_pylast.yaml
|
||||||
lastfm.txt.pkl
|
lastfm.txt.pkl
|
||||||
secrets.sh
|
secrets.sh
|
||||||
|
|
||||||
|
.dir-locals.el
|
31
README.md
31
README.md
|
@ -11,12 +11,10 @@ Try using the pydoc utility for help on usage or see [test_pylast.py](test_pylas
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
The easiest way is via pip:
|
Install via pip:
|
||||||
|
|
||||||
pip install pylast
|
pip install pylast
|
||||||
|
|
||||||
Or copy [pylast.py](pylast.py) to somewhere your Python can see it. No other dependencies are needed.
|
|
||||||
|
|
||||||
|
|
||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
|
@ -27,7 +25,6 @@ Features
|
||||||
* Full object-oriented design.
|
* Full object-oriented design.
|
||||||
* Proxy support.
|
* Proxy support.
|
||||||
* Internal caching support for some web services calls (disabled by default).
|
* Internal caching support for some web services calls (disabled by default).
|
||||||
* No extra dependencies but Python itself.
|
|
||||||
* Support for other API-compatible networks like Libre.fm.
|
* Support for other API-compatible networks like Libre.fm.
|
||||||
* Python 3-friendly (Starting from 0.5).
|
* Python 3-friendly (Starting from 0.5).
|
||||||
|
|
||||||
|
@ -70,9 +67,9 @@ More examples in <a href="https://github.com/hugovk/lastfm-tools">hugovk/lastfm-
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
[test_pylast.py](test_pylast.py) contains integration tests with Last.fm, and plenty of code examples.
|
[tests/test_pylast.py](tests/test_pylast.py) contains integration tests with Last.fm, and plenty of code examples. Unit tests are also in the [tests/](tests/) directory.
|
||||||
|
|
||||||
You need a test account at Last.fm that will be cluttered with test data, and an API key and secret. Either copy [example_test_pylast.yaml](example_test_pylast.yaml) to test_pylast.yaml and fill out the credentials, or set them as environment variables like:
|
For integration tests you need a test account at Last.fm that will be cluttered with test data, and an API key and secret. Either copy [example_test_pylast.yaml](example_test_pylast.yaml) to test_pylast.yaml and fill out the credentials, or set them as environment variables like:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE
|
export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE
|
||||||
|
@ -81,31 +78,21 @@ export PYLAST_API_KEY=TODO_ENTER_YOURS_HERE
|
||||||
export PYLAST_API_SECRET=TODO_ENTER_YOURS_HERE
|
export PYLAST_API_SECRET=TODO_ENTER_YOURS_HERE
|
||||||
```
|
```
|
||||||
|
|
||||||
To run all:
|
To run all unit and integration tests:
|
||||||
```sh
|
```sh
|
||||||
pip install -r test_requirements.txt
|
pip install pytest
|
||||||
./test_pylast.py
|
py.test
|
||||||
```
|
```
|
||||||
|
|
||||||
Or run just one:
|
Or run just one test case:
|
||||||
```sh
|
```sh
|
||||||
./test_pylast.py -1 test_scrobble
|
py.test -k test_scrobble
|
||||||
```
|
|
||||||
|
|
||||||
Or all those tests matching a term:
|
|
||||||
```sh
|
|
||||||
./test_pylast.py -m geo
|
|
||||||
```
|
```
|
||||||
|
|
||||||
To run with coverage:
|
To run with coverage:
|
||||||
```sh
|
```sh
|
||||||
coverage run --source=pylast ./test_pylast.py
|
py.test -v --cov pylast --cov-report term-missing
|
||||||
coverage report # for command-line report
|
coverage report # for command-line report
|
||||||
coverage html # for HTML report
|
coverage html # for HTML report
|
||||||
open htmlcov/index.html
|
open htmlcov/index.html
|
||||||
```
|
```
|
||||||
|
|
||||||
To perform some static analysis:
|
|
||||||
```sh
|
|
||||||
./check.sh
|
|
||||||
```
|
|
||||||
|
|
4
clonedigger.sh
Executable file
4
clonedigger.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
clonedigger pylast
|
||||||
|
grep -E "Clones detected|lines are duplicates" output.html
|
||||||
|
exit 0
|
|
@ -20,7 +20,7 @@
|
||||||
#
|
#
|
||||||
# http://code.google.com/p/pylast/
|
# http://code.google.com/p/pylast/
|
||||||
|
|
||||||
__version__ = '1.0.0'
|
__version__ = '1.0.1'
|
||||||
__author__ = 'Amr Hassan, hugovk'
|
__author__ = 'Amr Hassan, hugovk'
|
||||||
__copyright__ = "Copyright (C) 2008-2010 Amr Hassan, 2013-2014 hugovk"
|
__copyright__ = "Copyright (C) 2008-2010 Amr Hassan, 2013-2014 hugovk"
|
||||||
__license__ = "apache2"
|
__license__ = "apache2"
|
||||||
|
@ -37,6 +37,8 @@ import collections
|
||||||
import warnings
|
import warnings
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
def _deprecation_warning(message):
|
def _deprecation_warning(message):
|
||||||
warnings.warn(message, DeprecationWarning)
|
warnings.warn(message, DeprecationWarning)
|
||||||
|
@ -1326,9 +1328,9 @@ class _BaseObject(object):
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
# Convert any ints (or whatever) into strings
|
# Convert any ints (or whatever) into strings
|
||||||
values = map(str, self._get_params().values())
|
values = map(six.text_type, self._get_params().values())
|
||||||
|
|
||||||
return hash(self.network) + hash(str(type(self)) + "".join(
|
return hash(self.network) + hash(six.text_type(type(self)) + "".join(
|
||||||
list(self._get_params().keys()) + list(values)
|
list(self._get_params().keys()) + list(values)
|
||||||
).lower())
|
).lower())
|
||||||
|
|
||||||
|
@ -1910,9 +1912,12 @@ class Artist(_BaseObject, _Taggable):
|
||||||
return "pylast.Artist(%s, %s)" % (
|
return "pylast.Artist(%s, %s)" % (
|
||||||
repr(self.get_name()), repr(self.network))
|
repr(self.get_name()), repr(self.network))
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return six.text_type(self.get_name())
|
||||||
|
|
||||||
@_string_output
|
@_string_output
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.get_name()
|
return self.__unicode__()
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if type(self) is type(other):
|
if type(self) is type(other):
|
||||||
|
@ -3966,40 +3971,22 @@ def md5(text):
|
||||||
|
|
||||||
|
|
||||||
def _unicode(text):
|
def _unicode(text):
|
||||||
if sys.version_info[0] == 3:
|
if isinstance(text, six.binary_type):
|
||||||
if type(text) in (bytes, bytearray):
|
return six.text_type(text, "utf-8")
|
||||||
return str(text, "utf-8")
|
elif isinstance(text, six.text_type):
|
||||||
elif type(text) == str:
|
return text
|
||||||
return text
|
else:
|
||||||
else:
|
return six.text_type(text)
|
||||||
return str(text)
|
|
||||||
|
|
||||||
elif sys.version_info[0] == 2:
|
|
||||||
if type(text) in (str,):
|
|
||||||
return unicode(text, "utf-8")
|
|
||||||
elif type(text) == unicode:
|
|
||||||
return text
|
|
||||||
else:
|
|
||||||
return unicode(text)
|
|
||||||
|
|
||||||
|
|
||||||
def _string(text):
|
def _string(string):
|
||||||
"""For Python2 routines that can only process str type."""
|
"""For Python2 routines that can only process str type."""
|
||||||
|
if isinstance(string, str):
|
||||||
if sys.version_info[0] == 3:
|
return string
|
||||||
if type(text) != str:
|
casted = six.text_type(string)
|
||||||
return str(text)
|
if sys.version_info[0] == 2:
|
||||||
else:
|
casted = casted.encode("utf-8")
|
||||||
return text
|
return casted
|
||||||
|
|
||||||
elif sys.version_info[0] == 2:
|
|
||||||
if type(text) == str:
|
|
||||||
return text
|
|
||||||
|
|
||||||
if type(text) == int:
|
|
||||||
return str(text)
|
|
||||||
|
|
||||||
return text.encode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_nodes(limit, sender, method_name, cacheable, params=None):
|
def _collect_nodes(limit, sender, method_name, cacheable, params=None):
|
||||||
|
|
7
setup.py
7
setup.py
|
@ -6,8 +6,9 @@ from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="pylast",
|
name="pylast",
|
||||||
version="1.0.0",
|
version="1.0.1",
|
||||||
author="Amr Hassan <amr.hassan@gmail.com>",
|
author="Amr Hassan <amr.hassan@gmail.com>",
|
||||||
|
install_requires=['six'],
|
||||||
tests_require=['mock', 'pytest', 'coverage', 'pep8', 'pyyaml', 'pyflakes'],
|
tests_require=['mock', 'pytest', 'coverage', 'pep8', 'pyyaml', 'pyflakes'],
|
||||||
description=("A Python interface to Last.fm "
|
description=("A Python interface to Last.fm "
|
||||||
"(and other API compatible social networks)"),
|
"(and other API compatible social networks)"),
|
||||||
|
@ -24,9 +25,9 @@ setup(
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.3",
|
"Programming Language :: Python :: 3.3",
|
||||||
"Programming Language :: Python :: 3.4",
|
"Programming Language :: Python :: 3.4",
|
||||||
],
|
],
|
||||||
keywords=["Last.fm", "music", "scrobble", "scrobbling"],
|
keywords=["Last.fm", "music", "scrobble", "scrobbling"],
|
||||||
packages=find_packages(exclude=('tests*')),
|
packages=find_packages(exclude=('tests*',)),
|
||||||
license="Apache2"
|
license="Apache2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import mock
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import pylast
|
|
||||||
|
|
||||||
|
|
||||||
def mock_network():
|
|
||||||
return mock.Mock(
|
|
||||||
_get_ws_auth=mock.Mock(return_value=("", "", ""))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('unicode_artist', [u'\xe9lafdasfdsafdsa', u'ééééééé'])
|
|
||||||
def test_get_cache_key(unicode_artist):
|
|
||||||
request = pylast._Request(mock_network(), 'some_method',
|
|
||||||
params={'artist': unicode_artist})
|
|
||||||
request._get_cache_key()
|
|
|
@ -3,6 +3,7 @@
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import pytest
|
||||||
from random import choice
|
from random import choice
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
@ -19,10 +20,13 @@ def load_secrets():
|
||||||
doc = yaml.load(f)
|
doc = yaml.load(f)
|
||||||
else:
|
else:
|
||||||
doc = {}
|
doc = {}
|
||||||
doc["username"] = os.environ['PYLAST_USERNAME'].strip()
|
try:
|
||||||
doc["password_hash"] = os.environ['PYLAST_PASSWORD_HASH'].strip()
|
doc["username"] = os.environ['PYLAST_USERNAME'].strip()
|
||||||
doc["api_key"] = os.environ['PYLAST_API_KEY'].strip()
|
doc["password_hash"] = os.environ['PYLAST_PASSWORD_HASH'].strip()
|
||||||
doc["api_secret"] = os.environ['PYLAST_API_SECRET'].strip()
|
doc["api_key"] = os.environ['PYLAST_API_KEY'].strip()
|
||||||
|
doc["api_secret"] = os.environ['PYLAST_API_SECRET'].strip()
|
||||||
|
except KeyError:
|
||||||
|
pytest.skip("Missing environment variables: PYLAST_USERNAME etc.")
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
|
|
29
tests/unicode_test.py
Normal file
29
tests/unicode_test.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import mock
|
||||||
|
import pytest
|
||||||
|
import six
|
||||||
|
|
||||||
|
import pylast
|
||||||
|
|
||||||
|
|
||||||
|
def mock_network():
|
||||||
|
return mock.Mock(
|
||||||
|
_get_ws_auth=mock.Mock(return_value=("", "", ""))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('artist', [
|
||||||
|
u'\xe9lafdasfdsafdsa', u'ééééééé',
|
||||||
|
pylast.Artist(u'B\xe9l', mock_network()),
|
||||||
|
'fdasfdsafsaf not unicode',
|
||||||
|
])
|
||||||
|
def test_get_cache_key(artist):
|
||||||
|
request = pylast._Request(mock_network(), 'some_method',
|
||||||
|
params={'artist': artist})
|
||||||
|
request._get_cache_key()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('obj', [pylast.Artist(u'B\xe9l', mock_network())])
|
||||||
|
def test_cast_and_hash(obj):
|
||||||
|
assert type(six.text_type(obj)) is six.text_type
|
||||||
|
assert isinstance(hash(obj), int)
|
3
tox.ini
3
tox.ini
|
@ -8,6 +8,7 @@ deps =
|
||||||
pyyaml
|
pyyaml
|
||||||
pytest
|
pytest
|
||||||
mock
|
mock
|
||||||
|
ipdb
|
||||||
pytest-cov
|
pytest-cov
|
||||||
commands = py.test -v --cov pylast --cov-report term-missing {posargs}
|
commands = py.test -v --cov pylast --cov-report term-missing {posargs}
|
||||||
|
|
||||||
|
@ -27,4 +28,4 @@ commands =
|
||||||
pyflakes tests
|
pyflakes tests
|
||||||
pep8 pylast
|
pep8 pylast
|
||||||
pep8 tests
|
pep8 tests
|
||||||
clonedigger pylast -o /dev/stdout | grep -E "Clones detected\|lines are duplicates"
|
./clonedigger.sh
|
Loading…
Reference in a new issue