Merge remote-tracking branch 'pylast/master'

This commit is contained in:
Koen van Zuijlen 2020-12-24 20:43:06 +01:00
commit 7193eb0d1f
23 changed files with 317 additions and 320 deletions

6
.github/labels.yml vendored
View file

@ -97,6 +97,12 @@
- color: 0366d6 - color: 0366d6
description: "For dependencies" description: "For dependencies"
name: dependencies name: dependencies
- color: f4660e
description: ""
name: Hacktoberfest
- color: f4660e
description: "To credit accepted Hacktoberfest PRs"
name: hacktoberfest-accepted
- color: fef2c0 - color: fef2c0
description: "" description: ""
name: test name: test

View file

@ -1,5 +1,5 @@
name-template: "$NEXT_PATCH_VERSION" name-template: "Release $RESOLVED_VERSION"
tag-template: "$NEXT_PATCH_VERSION" tag-template: "$RESOLVED_VERSION"
categories: categories:
- title: "Added" - title: "Added"
@ -26,3 +26,20 @@ template: |
## Changes ## Changes
$CHANGES $CHANGES
version-resolver:
major:
labels:
- "changelog: Removed"
minor:
labels:
- "changelog: Added"
- "changelog: Changed"
- "changelog: Deprecated"
- "enhancement"
patch:
labels:
- "changelog: Fixed"
- "bug"
default: minor

57
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,57 @@
name: Deploy
on:
push:
branches:
- master
release:
types:
- published
jobs:
build:
if: github.repository == 'pylast/pylast'
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Cache
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: deploy-${{ hashFiles('**/setup.py') }}
restore-keys: |
deploy-
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U setuptools twine wheel
- name: Build package
run: |
python setup.py --version
python setup.py sdist --format=gztar bdist_wheel
twine check dist/*
- name: Publish package to PyPI
if: github.event.action == 'published'
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.pypi_password }}
- name: Publish package to TestPyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.test_pypi_password }}
repository_url: https://test.pypi.org/legacy/

View file

@ -11,5 +11,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: micnncim/action-label-syncer@v1 - uses: micnncim/action-label-syncer@v1
with:
prune: false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -4,34 +4,9 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-18.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Cache - uses: pre-commit/action@v2.0.0
uses: actions/cache@v2
with:
path: |
~/.cache/pip
~/.cache/pre-commit
key:
lint-v2-${{ hashFiles('**/setup.py') }}-${{
hashFiles('**/.pre-commit-config.yaml') }}
restore-keys: |
lint-v2-
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U tox
- name: Lint
run: tox -e lint
env:
PRE_COMMIT_COLOR: always

63
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,63 @@
name: Test
on: [push, pull_request]
env:
FORCE_COLOR: 1
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev", "pypy3"]
os: [ubuntu-20.04]
include:
# Include new variables for Codecov
- { codecov-flag: GHA_Ubuntu2004, os: ubuntu-20.04 }
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.os }}-${{ matrix.python-version }}-v3-${{
hashFiles('**/setup.py') }}
restore-keys: |
${{ matrix.os }}-${{ matrix.python-version }}-v3-
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U wheel
python -m pip install -U tox
- name: Tox tests
shell: bash
run: |
tox -e py
env:
PYLAST_API_KEY: ${{ secrets.PYLAST_API_KEY }}
PYLAST_API_SECRET: ${{ secrets.PYLAST_API_SECRET }}
PYLAST_PASSWORD_HASH: ${{ secrets.PYLAST_PASSWORD_HASH }}
PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }}
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
flags: ${{ matrix.codecov-flag }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}

View file

@ -3,8 +3,6 @@ pull_request_rules:
conditions: conditions:
- label=automerge - label=automerge
- status-success=build - status-success=build
- status-success=continuous-integration/travis-ci/pr
- status-success=continuous-integration/travis-ci/push
actions: actions:
merge: merge:
method: merge method: merge

View file

@ -1,42 +1,42 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.6.1 rev: v2.7.4
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: ["--py3-plus"] args: ["--py36-plus"]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 19.10b0 rev: 20.8b1
hooks: hooks:
- id: black - id: black
args: ["--target-version", "py35"] args: ["--target-version", "py36"]
# override until resolved: https://github.com/psf/black/issues/402 # override until resolved: https://github.com/psf/black/issues/402
files: \.pyi?$ files: \.pyi?$
types: [] types: []
- repo: https://github.com/PyCQA/isort
rev: 5.6.4
hooks:
- id: isort
- repo: https://gitlab.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 3.8.3 rev: 3.8.4
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat] additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
- repo: https://github.com/asottile/seed-isort-config
rev: v2.2.0
hooks:
- id: seed-isort-config
- repo: https://github.com/timothycrosley/isort
rev: 4.3.21
hooks:
- id: isort
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.5.1 rev: v1.7.0
hooks: hooks:
- id: python-check-blanket-noqa - id: python-check-blanket-noqa
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0 rev: v3.3.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-yaml - id: check-yaml
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 0.5.0
hooks:
- id: tox-ini-fmt

View file

@ -1,67 +0,0 @@
language: python
cache:
pip: true
directories:
- $HOME/.cache/pre-commit
env:
global:
- secure: ivg6II471E9HV8xyqnawLIuP/sZ0J63Y+BC0BQcRVKtLn/K3zmD1ozM3TFL9S549Nxd0FqDKHXJvXsgaTGIDpK8sxE2AMKV5IojyM0iAVuN7YjPK9vwSlRw1u0EysPMFqxOZVQnoDyHrSGIUrP/VMdnhBu6dbUX0FyEkvZshXhY=
- secure: gDWNEYA1EUv4G230/KzcTgcmEST0nf2FeW/z/prsoQBu+TWw1rKKSJAJeMLvuI1z4aYqqNYdmqjWyNhhVK3p5wmFP2lxbhaBT1jDsxxFpePc0nUkdAQOOD0yBpbBGkqkjjxU34HjTX2NFNEbcM3izVVE9oQmS5r4oFFNJgdL91c=
- secure: RpsZblHFU7a5dnkO/JUgi70RkNJwoUh3jJqVo1oOLjL+lvuAmPXhI8MDk2diUk43X+XCBFBEnm7UCGnjUF+hDnobO4T+VrIFuVJWg3C7iKIT+YWvgG6A+CSeo/P0I0dAeUscTr5z4ylOq3EDx4MFSa8DmoWMmjKTAG1GAeTlY2k=
- secure: T5OKyd5Bs0nZbUr+YICbThC5GrFq/kUjX8FokzCv7NWsYaUWIwEmMXXzoYALoB3A+rAglOx6GABaupoNKKg3tFQyxXphuMKpZ8MasMAMFjFW0d7wsgGy0ylhVwrgoKzDbCQ5FKbohC+9ltLs+kKMCQ0L+MI70a/zTfF4/dVWO/o=
- secure: DxBvGGoIgbAeuuU3A6+J1HBbmUAEvqdmK73etw+yNKDLGvvukgTL33dNCr8CZXLKRRvfhrjU7Q01GUpOTxrVQ9nJgsD55kwx0wPtuBWIF80M2m4SPsiVLlwP/LFYD5JMDTDWjFTlVahma8P7qoLjCc7b/RgigWLidH19snQmjdY=
- secure: VPARlWNg/0Nit7a924vJlDfv7yiuTDtrcGZNFrZ6yN3dl8ZjVPizQXQNKA3yq0y2jW25nwjRwZYj3eY5MdM9F7Sw51d+/8AjFtdCuRgDvwlQFR/pCoyzqgJATkXKo7mlejvnA+5EKUzAmu3drIbboFgbLgRTMrG7b/ot9tazTHs=
- secure: CQYL7MH6tSVrCcluIfWfDSTo4E/p+9pF0eI7Vtf0oaZBzyulODHK8h/mzJp4HwezyfOu0RCedq6sloGQr1/29CvWWESaYyoGoGz9Mz2ZS+MpIcjGISfZa+x4vSp6QPFvd4i/1Z/1j2gJVVyswkrIVUwZIDJtfAKzZI5iHx2gH8Y=
- secure: SsKJoJwtDVWrL5xxl9C/gTRy6FhfRQQNNAFOogl9mTs/WeI2t9QTYoKsxLPXOdoRdu4MvT3h/B2sjwggt7zP81fBVxQRTkg4nq0zSHlj0NqclbFa6I5lUYdGwH9gPk/HWJJwXhKRDsqn/iRw2v+qBDs/j3kIgPQ0yjM58LEPXic=
matrix:
fast_finish: true
include:
- python: 3.8
env: TOXENV=lint
- python: 3.8
- python: 3.7
- python: 3.6
- python: 3.5
- python: 3.9-dev
- python: 3.10-dev
- python: pypy3
install:
- travis_retry pip install -U pip
- travis_retry pip install -U tox-travis
script: tox
after_success:
- |
if [ "$TOXENV" != "lint" ]; then
travis_retry pip install -U coveralls && coveralls
travis_retry pip install -U codecov && codecov
fi
deploy:
- provider: pypi
server: https://test.pypi.org/legacy/
on:
tags: false
repo: pylast/pylast
branch: master
condition: $TOXENV = lint
user: hugovk
password:
secure: "OCNT7Sf7TpS6aKuqBXEWxJZjmEpdERTBp/yllOd9xnpFt2ZL96CyKtAhPA8zu5OP58QFEZSafZRfXYJoz78RDrx3gOdRXCFT00vXIMnjVvrAlieNEHCVAT0kRW9lYK1Cf5baHYsOYIs6EZf2fEAhdzvmh83G4Y1Y+FPR9tA6uy8="
distributions: sdist --format=gztar bdist_wheel
skip_existing: true
- provider: pypi
on:
tags: true
repo: pylast/pylast
branch: master
condition: $TOXENV = lint
user: hugovk
password:
secure: "OCNT7Sf7TpS6aKuqBXEWxJZjmEpdERTBp/yllOd9xnpFt2ZL96CyKtAhPA8zu5OP58QFEZSafZRfXYJoz78RDrx3gOdRXCFT00vXIMnjVvrAlieNEHCVAT0kRW9lYK1Cf5baHYsOYIs6EZf2fEAhdzvmh83G4Y1Y+FPR9tA6uy8="
distributions: sdist --format=gztar bdist_wheel
skip_existing: true

View file

@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.0.0] - 2020-10-07
## Added
* Add support for Python 3.9 (#347) @hugovk
## Removed
* Remove deprecated `Artist.get_cover_image`, `User.get_artist_tracks` and `STATUS_TOKEN_ERROR` (#348) @hugovk
* Drop support for EOL Python 3.5 (#346) @hugovk
## [3.3.0] - 2020-06-25 ## [3.3.0] - 2020-06-25
### Added ### Added
@ -86,10 +96,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Support for Python 2.7 ([#265]) * Support for Python 2.7 ([#265])
[3.3.0]: https://github.com/pylast/pylast/compare/v3.2.1...3.3.0 [4.0.0]: https://github.com/pylast/pylast/compare/3.3.0...4.0.0
[3.2.1]: https://github.com/pylast/pylast/compare/v3.2.0...3.2.1 [3.3.0]: https://github.com/pylast/pylast/compare/3.2.1...3.3.0
[3.2.0]: https://github.com/pylast/pylast/compare/v3.1.0...3.2.0 [3.2.1]: https://github.com/pylast/pylast/compare/3.2.0...3.2.1
[3.1.0]: https://github.com/pylast/pylast/compare/v3.0.0...3.1.0 [3.2.0]: https://github.com/pylast/pylast/compare/3.1.0...3.2.0
[3.1.0]: https://github.com/pylast/pylast/compare/3.0.0...3.1.0
[3.0.0]: https://github.com/pylast/pylast/compare/2.4.0...3.0.0 [3.0.0]: https://github.com/pylast/pylast/compare/2.4.0...3.0.0
[2.4.0]: https://github.com/pylast/pylast/compare/2.3.0...2.4.0 [2.4.0]: https://github.com/pylast/pylast/compare/2.3.0...2.4.0
[#265]: https://github.com/pylast/pylast/issues/265 [#265]: https://github.com/pylast/pylast/issues/265
@ -105,3 +116,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#311]: https://github.com/pylast/pylast/issues/311 [#311]: https://github.com/pylast/pylast/issues/311
[#312]: https://github.com/pylast/pylast/issues/312 [#312]: https://github.com/pylast/pylast/issues/312
[#316]: https://github.com/pylast/pylast/issues/316 [#316]: https://github.com/pylast/pylast/issues/316
[#346]: https://github.com/pylast/pylast/issues/346
[#347]: https://github.com/pylast/pylast/issues/347
[#348]: https://github.com/pylast/pylast/issues/348

View file

@ -4,10 +4,9 @@ pyLast
[![PyPI version](https://img.shields.io/pypi/v/pylast.svg)](https://pypi.org/project/pylast/) [![PyPI version](https://img.shields.io/pypi/v/pylast.svg)](https://pypi.org/project/pylast/)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/pylast.svg)](https://pypi.org/project/pylast/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/pylast.svg)](https://pypi.org/project/pylast/)
[![PyPI downloads](https://img.shields.io/pypi/dm/pylast.svg)](https://pypistats.org/packages/pylast) [![PyPI downloads](https://img.shields.io/pypi/dm/pylast.svg)](https://pypistats.org/packages/pylast)
[![Build status](https://travis-ci.org/pylast/pylast.svg?branch=master)](https://travis-ci.org/pylast/pylast) [![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions)
[![Coverage (Codecov)](https://codecov.io/gh/pylast/pylast/branch/master/graph/badge.svg)](https://codecov.io/gh/pylast/pylast) [![Coverage (Codecov)](https://codecov.io/gh/pylast/pylast/branch/master/graph/badge.svg)](https://codecov.io/gh/pylast/pylast)
[![Coverage (Coveralls)](https://coveralls.io/repos/github/pylast/pylast/badge.svg?branch=master)](https://coveralls.io/github/pylast/pylast?branch=master) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
[![DOI](https://zenodo.org/badge/7803088.svg)](https://zenodo.org/badge/latestdoi/7803088) [![DOI](https://zenodo.org/badge/7803088.svg)](https://zenodo.org/badge/latestdoi/7803088)
A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites such as [Libre.fm](https://libre.fm/). A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites such as [Libre.fm](https://libre.fm/).
@ -31,11 +30,13 @@ Or from requirements.txt:
Note: Note:
* pylast 3.0.0+ supports Python 3.5+ ([#265](https://github.com/pylast/pylast/issues/265)) * pyLast 4.0.0+ supports Python 3.6-3.9.
* pyLast 2.2.0 - 2.4.0 supports Python 2.7.10+, 3.4, 3.5, 3.6, 3.7. * pyLast 3.2.0 - 3.3.0 supports Python 3.5-3.8.
* pyLast 2.0.0 - 2.1.0 supports Python 2.7.10+, 3.4, 3.5, 3.6. * pyLast 3.0.0 - 3.1.0 supports Python 3.5-3.7.
* pyLast 1.7.0 - 1.9.0 supports Python 2.7, 3.3, 3.4, 3.5, 3.6. * pyLast 2.2.0 - 2.4.0 supports Python 2.7.10+, 3.4-3.7.
* pyLast 1.0.0 - 1.6.0 supports Python 2.7, 3.3, 3.4. * pyLast 2.0.0 - 2.1.0 supports Python 2.7.10+, 3.4-3.6.
* pyLast 1.7.0 - 1.9.0 supports Python 2.7, 3.3-3.6.
* pyLast 1.0.0 - 1.6.0 supports Python 2.7, 3.3-3.4.
* pyLast 0.5 supports Python 2, 3. * pyLast 0.5 supports Python 2, 3.
* pyLast < 0.5 supports Python 2. * pyLast < 0.5 supports Python 2.
@ -49,7 +50,6 @@ Features
* 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).
* 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).
Getting started Getting started

View file

@ -1,8 +1,9 @@
# Release Checklist # Release Checklist
* [ ] Get master to the appropriate code release state. * [ ] Get master to the appropriate code release state.
[Travis CI](https://travis-ci.org/pylast/pylast) should be running cleanly for [GitHub Actions](https://github.com/pylast/pylast/actions) should be running cleanly for
all merges to master. all merges to master.
[![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions)
* [ ] Edit release draft, adjust text if needed: * [ ] Edit release draft, adjust text if needed:
https://github.com/pylast/pylast/releases https://github.com/pylast/pylast/releases
@ -13,8 +14,8 @@
* [ ] Publish release * [ ] Publish release
* [ ] Check the tagged [Travis CI build](https://travis-ci.org/pylast/pylast) has * [ ] Check the tagged [GitHub Actions build](https://github.com/pylast/pylast/actions?query=workflow%3ADeploy)
deployed to [PyPI](https://pypi.org/project/pylast/#history) has deployed to [PyPI](https://pypi.org/project/pylast/#history)
* [ ] Check installation: * [ ] Check installation:

View file

@ -1,6 +1,5 @@
[flake8] [flake8]
ignore = W503
max_line_length = 88 max_line_length = 88
[tool:isort] [tool:isort]
known_third_party = flaky,pkg_resources,pylast,pytest,setuptools profile = black

View file

@ -27,7 +27,7 @@ setup(
extras_require={ extras_require={
"tests": ["flaky", "pytest", "pytest-cov", "pytest-random-order", "pyyaml"] "tests": ["flaky", "pytest", "pytest-cov", "pytest-random-order", "pyyaml"]
}, },
python_requires=">=3.5", python_requires=">=3.6",
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: Apache Software License", "License :: OSI Approved :: Apache Software License",
@ -35,10 +35,10 @@ setup(
"Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",

View file

@ -27,7 +27,6 @@ import shelve
import ssl import ssl
import tempfile import tempfile
import time import time
import warnings
import xml.dom import xml.dom
from http.client import HTTPSConnection from http.client import HTTPSConnection
from urllib.parse import quote_plus from urllib.parse import quote_plus
@ -49,9 +48,7 @@ STATUS_AUTH_FAILED = 4
STATUS_INVALID_FORMAT = 5 STATUS_INVALID_FORMAT = 5
STATUS_INVALID_PARAMS = 6 STATUS_INVALID_PARAMS = 6
STATUS_INVALID_RESOURCE = 7 STATUS_INVALID_RESOURCE = 7
# DeprecationWarning: STATUS_TOKEN_ERROR is deprecated and will be STATUS_OPERATION_FAILED = 8
# removed in a future version. Use STATUS_OPERATION_FAILED instead.
STATUS_OPERATION_FAILED = STATUS_TOKEN_ERROR = 8
STATUS_INVALID_SK = 9 STATUS_INVALID_SK = 9
STATUS_INVALID_API_KEY = 10 STATUS_INVALID_API_KEY = 10
STATUS_OFFLINE = 11 STATUS_OFFLINE = 11
@ -146,29 +143,29 @@ class _Network:
token=None, token=None,
): ):
""" """
name: the name of the network name: the name of the network
homepage: the homepage URL homepage: the homepage URL
ws_server: the URL of the webservices server ws_server: the URL of the webservices server
api_key: a provided API_KEY api_key: a provided API_KEY
api_secret: a provided API_SECRET api_secret: a provided API_SECRET
session_key: a generated session_key or None session_key: a generated session_key or None
username: a username of a valid user username: a username of a valid user
password_hash: the output of pylast.md5(password) where password is password_hash: the output of pylast.md5(password) where password is
the user's password the user's password
domain_names: a dict mapping each DOMAIN_* value to a string domain domain_names: a dict mapping each DOMAIN_* value to a string domain
name name
urls: a dict mapping types to URLs urls: a dict mapping types to URLs
token: an authentication token to retrieve a session token: an authentication token to retrieve a session
if username and password_hash were provided and not session_key, if username and password_hash were provided and not session_key,
session_key will be generated automatically when needed. session_key will be generated automatically when needed.
Either a valid session_key or a combination of username and Either a valid session_key or a combination of username and
password_hash must be present for scrobbling. password_hash must be present for scrobbling.
You should use a preconfigured network object through a You should use a preconfigured network object through a
get_*_network(...) method instead of creating an object get_*_network(...) method instead of creating an object
of this class, unless you know what you're doing. of this class, unless you know what you're doing.
""" """
self.name = name self.name = name
@ -209,56 +206,56 @@ class _Network:
def get_artist(self, artist_name): def get_artist(self, artist_name):
""" """
Return an Artist object Return an Artist object
""" """
return Artist(artist_name, self) return Artist(artist_name, self)
def get_track(self, artist, title): def get_track(self, artist, title):
""" """
Return a Track object Return a Track object
""" """
return Track(artist, title, self) return Track(artist, title, self)
def get_album(self, artist, title): def get_album(self, artist, title):
""" """
Return an Album object Return an Album object
""" """
return Album(artist, title, self) return Album(artist, title, self)
def get_authenticated_user(self): def get_authenticated_user(self):
""" """
Returns the authenticated user Returns the authenticated user
""" """
return AuthenticatedUser(self) return AuthenticatedUser(self)
def get_country(self, country_name): def get_country(self, country_name):
""" """
Returns a country object Returns a country object
""" """
return Country(country_name, self) return Country(country_name, self)
def get_user(self, username): def get_user(self, username):
""" """
Returns a user object Returns a user object
""" """
return User(username, self) return User(username, self)
def get_tag(self, name): def get_tag(self, name):
""" """
Returns a tag object Returns a tag object
""" """
return Tag(name, self) return Tag(name, self)
def _get_language_domain(self, domain_language): def _get_language_domain(self, domain_language):
""" """
Returns the mapped domain name of the network to a DOMAIN_* value Returns the mapped domain name of the network to a DOMAIN_* value
""" """
if domain_language in self.domain_names: if domain_language in self.domain_names:
@ -271,13 +268,13 @@ class _Network:
def _get_ws_auth(self): def _get_ws_auth(self):
""" """
Returns an (API_KEY, API_SECRET, SESSION_KEY) tuple. Returns an (API_KEY, API_SECRET, SESSION_KEY) tuple.
""" """
return self.api_key, self.api_secret, self.session_key return self.api_key, self.api_secret, self.session_key
def _delay_call(self): def _delay_call(self):
""" """
Makes sure that web service calls are at least 0.2 seconds apart. Makes sure that web service calls are at least 0.2 seconds apart.
""" """
now = time.time() now = time.time()
@ -1416,31 +1413,31 @@ class WSError(Exception):
def get_id(self): def get_id(self):
"""Returns the exception ID, from one of the following: """Returns the exception ID, from one of the following:
STATUS_INVALID_SERVICE = 2 STATUS_INVALID_SERVICE = 2
STATUS_INVALID_METHOD = 3 STATUS_INVALID_METHOD = 3
STATUS_AUTH_FAILED = 4 STATUS_AUTH_FAILED = 4
STATUS_INVALID_FORMAT = 5 STATUS_INVALID_FORMAT = 5
STATUS_INVALID_PARAMS = 6 STATUS_INVALID_PARAMS = 6
STATUS_INVALID_RESOURCE = 7 STATUS_INVALID_RESOURCE = 7
STATUS_OPERATION_FAILED = 8 STATUS_OPERATION_FAILED = 8
STATUS_INVALID_SK = 9 STATUS_INVALID_SK = 9
STATUS_INVALID_API_KEY = 10 STATUS_INVALID_API_KEY = 10
STATUS_OFFLINE = 11 STATUS_OFFLINE = 11
STATUS_SUBSCRIBERS_ONLY = 12 STATUS_SUBSCRIBERS_ONLY = 12
STATUS_TOKEN_UNAUTHORIZED = 14 STATUS_TOKEN_UNAUTHORIZED = 14
STATUS_TOKEN_EXPIRED = 15 STATUS_TOKEN_EXPIRED = 15
STATUS_TEMPORARILY_UNAVAILABLE = 16 STATUS_TEMPORARILY_UNAVAILABLE = 16
STATUS_LOGIN_REQUIRED = 17 STATUS_LOGIN_REQUIRED = 17
STATUS_TRIAL_EXPIRED = 18 STATUS_TRIAL_EXPIRED = 18
STATUS_NOT_ENOUGH_CONTENT = 20 STATUS_NOT_ENOUGH_CONTENT = 20
STATUS_NOT_ENOUGH_MEMBERS = 21 STATUS_NOT_ENOUGH_MEMBERS = 21
STATUS_NOT_ENOUGH_FANS = 22 STATUS_NOT_ENOUGH_FANS = 22
STATUS_NOT_ENOUGH_NEIGHBOURS = 23 STATUS_NOT_ENOUGH_NEIGHBOURS = 23
STATUS_NO_PEAK_RADIO = 24 STATUS_NO_PEAK_RADIO = 24
STATUS_RADIO_NOT_FOUND = 25 STATUS_RADIO_NOT_FOUND = 25
STATUS_API_KEY_SUSPENDED = 26 STATUS_API_KEY_SUSPENDED = 26
STATUS_DEPRECATED = 27 STATUS_DEPRECATED = 27
STATUS_RATE_LIMIT_EXCEEDED = 29 STATUS_RATE_LIMIT_EXCEEDED = 29
""" """
return self.status return self.status
@ -1721,32 +1718,6 @@ class Artist(_Taggable):
return _extract(self._request(self.ws_prefix + ".getCorrection"), "name") return _extract(self._request(self.ws_prefix + ".getCorrection"), "name")
def get_cover_image(self, size=SIZE_EXTRA_LARGE):
"""
Returns a URI to the cover image
size can be one of:
SIZE_MEGA
SIZE_EXTRA_LARGE
SIZE_LARGE
SIZE_MEDIUM
SIZE_SMALL
"""
warnings.warn(
"Artist.get_cover_image is deprecated and will be removed in a future "
"version. In the meantime, only default star images are available. "
"See https://github.com/pylast/pylast/issues/317 and "
"https://support.last.fm/t/api-announcement/202",
DeprecationWarning,
stacklevel=2,
)
if "image" not in self.info:
self.info["image"] = _extract_all(
self._request(self.ws_prefix + ".getInfo", cacheable=True), "image"
)
return self.info["image"][size]
def get_playcount(self): def get_playcount(self):
"""Returns the number of plays on the network.""" """Returns the number of plays on the network."""
@ -2273,38 +2244,7 @@ class User(_Chartable):
return self.name return self.name
def get_artist_tracks(self, artist, cacheable=False, stream=False): def get_friends(self, limit=50, cacheable=False):
"""
Deprecated by Last.fm.
Get a list of tracks by a given artist scrobbled by this user,
including scrobble time.
"""
warnings.warn(
"User.get_artist_tracks is deprecated and will be removed in a future "
"version. User.get_track_scrobbles is a partial replacement. "
"See https://github.com/pylast/pylast/issues/298",
DeprecationWarning,
stacklevel=2,
)
params = self._get_params()
params["artist"] = artist
def _get_artist_tracks():
for track_node in _collect_nodes(
None,
self,
self.ws_prefix + ".getArtistTracks",
cacheable,
params,
stream=stream,
):
yield self._extract_played_track(track_node=track_node)
return _get_artist_tracks() if stream else list(_get_artist_tracks())
def get_friends(self, limit=50, cacheable=False, stream=False):
"""Returns a list of the user's friends. """ """Returns a list of the user's friends. """
def _get_friends(): def _get_friends():
@ -2968,8 +2908,8 @@ def _url_safe(text):
def _number(string): def _number(string):
""" """
Extracts an int from a string. Extracts an int from a string.
Returns a 0 if None or an empty string was passed. Returns a 0 if None or an empty string was passed.
""" """
if not string: if not string:

View file

@ -2,9 +2,10 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
import pylast
import pytest import pytest
import pylast
from .test_pylast import WRITE_TEST, TestPyLastWithLastFm from .test_pylast import WRITE_TEST, TestPyLastWithLastFm
@ -153,11 +154,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
tags = artist.get_tags() tags = artist.get_tags()
assert len(tags) > 0 assert len(tags) > 0
found = False found = any(tag.name == "testing" for tag in tags)
for tag in tags:
if tag.name == "testing":
found = True
break
assert found assert found
@pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions")
@ -172,11 +169,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
tags = artist.get_tags() tags = artist.get_tags()
found = False found = any(tag.name == "testing" for tag in tags)
for tag in tags:
if tag.name == "testing":
found = True
break
assert not found assert not found
@pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions")
@ -191,11 +184,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
tags = artist.get_tags() tags = artist.get_tags()
found = False found = any(tag.name == "testing" for tag in tags)
for tag in tags:
if tag.name == "testing":
found = True
break
assert not found assert not found
@pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions")
@ -213,12 +202,8 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
tags_after = artist.get_tags() tags_after = artist.get_tags()
assert len(tags_after) == len(tags_before) - 2 assert len(tags_after) == len(tags_before) - 2
found1, found2 = False, False found1 = any(tag.name == "removetag1" for tag in tags_after)
for tag in tags_after: found2 = any(tag.name == "removetag2" for tag in tags_after)
if tag.name == "removetag1":
found1 = True
elif tag.name == "removetag2":
found2 = True
assert not found1 assert not found1
assert not found2 assert not found2
@ -256,16 +241,12 @@ class TestPyLastArtist(TestPyLastWithLastFm):
url = artist1.get_url() url = artist1.get_url()
mbid = artist1.get_mbid() mbid = artist1.get_mbid()
with pytest.warns(DeprecationWarning):
image = artist1.get_cover_image()
playcount = artist1.get_playcount() playcount = artist1.get_playcount()
streamable = artist1.is_streamable() streamable = artist1.is_streamable()
name = artist1.get_name(properly_capitalized=False) name = artist1.get_name(properly_capitalized=False)
name_cap = artist1.get_name(properly_capitalized=True) name_cap = artist1.get_name(properly_capitalized=True)
# Assert # Assert
assert "https" in image
assert playcount > 1 assert playcount > 1
assert artist1 != artist2 assert artist1 != artist2
assert name.lower() == name_cap.lower() assert name.lower() == name_cap.lower()
@ -308,4 +289,4 @@ class TestPyLastArtist(TestPyLastWithLastFm):
playcount = artist.get_userplaycount() playcount = artist.get_userplaycount()
# Assert # Assert
assert playcount >= 0 assert playcount >= 0 # whilst xfail: # pragma: no cover

View file

@ -2,9 +2,10 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
import pylast
from flaky import flaky from flaky import flaky
import pylast
from .test_pylast import PyLastTestCase, load_secrets from .test_pylast import PyLastTestCase, load_secrets

View file

@ -5,9 +5,10 @@ Integration (not unit) tests for pylast.py
import re import re
import time import time
import pylast
import pytest import pytest
import pylast
from .test_pylast import WRITE_TEST, TestPyLastWithLastFm from .test_pylast import WRITE_TEST, TestPyLastWithLastFm
@ -63,7 +64,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
self.network.enable_rate_limit() self.network.enable_rate_limit()
then = time.time() then = time.time()
# Make some network call, limit not applied first time # Make some network call, limit not applied first time
self.network.get_user(self.username) self.network.get_top_artists()
# Make a second network call, limiting should be applied # Make a second network call, limiting should be applied
self.network.get_top_artists() self.network.get_top_artists()
now = time.time() now = time.time()

View file

@ -6,14 +6,15 @@ import os
import sys import sys
import time import time
import pylast
import pytest import pytest
from flaky import flaky from flaky import flaky
import pylast
WRITE_TEST = sys.version_info[:2] == (3, 8) WRITE_TEST = sys.version_info[:2] == (3, 8)
def load_secrets(): def load_secrets(): # pragma: no cover
secrets_file = "test_pylast.yaml" secrets_file = "test_pylast.yaml"
if os.path.isfile(secrets_file): if os.path.isfile(secrets_file):
import yaml # pip install pyyaml import yaml # pip install pyyaml
@ -40,7 +41,12 @@ class PyLastTestCase:
assert str.endswith(suffix, start, end) assert str.endswith(suffix, start, end)
@flaky(max_runs=3, min_passes=1) def _no_xfail_rerun_filter(err, name, test, plugin):
for _ in test.iter_markers(name="xfail"):
return False
@flaky(max_runs=3, min_passes=1, rerun_filter=_no_xfail_rerun_filter)
class TestPyLastWithLastFm(PyLastTestCase): class TestPyLastWithLastFm(PyLastTestCase):
secrets = None secrets = None

View file

@ -4,9 +4,10 @@ Integration (not unit) tests for pylast.py
""" """
import time import time
import pylast
import pytest import pytest
import pylast
from .test_pylast import WRITE_TEST, TestPyLastWithLastFm from .test_pylast import WRITE_TEST, TestPyLastWithLastFm

View file

@ -7,10 +7,10 @@ import datetime as dt
import inspect import inspect
import os import os
import re import re
import warnings
import pytest
import pylast import pylast
import pytest
from .test_pylast import TestPyLastWithLastFm from .test_pylast import TestPyLastWithLastFm
@ -69,7 +69,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
if int(registered): if int(registered):
# Last.fm API broken? Used to be yyyy-mm-dd not Unix timestamp # Last.fm API broken? Used to be yyyy-mm-dd not Unix timestamp
assert registered == "1037793040" assert registered == "1037793040"
else: else: # pragma: no cover
# Old way # Old way
# Just check date because of timezones # Just check date because of timezones
assert "2002-11-20 " in registered assert "2002-11-20 " in registered
@ -193,8 +193,13 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Act/Assert # Act/Assert
self.helper_validate_cacheable(lastfm_user, "get_friends") self.helper_validate_cacheable(lastfm_user, "get_friends")
self.helper_validate_cacheable(lastfm_user, "get_loved_tracks") # no cover whilst xfail:
self.helper_validate_cacheable(lastfm_user, "get_recent_tracks") self.helper_validate_cacheable( # pragma: no cover
lastfm_user, "get_loved_tracks"
)
self.helper_validate_cacheable( # pragma: no cover
lastfm_user, "get_recent_tracks"
)
def test_user_get_top_tags_with_limit(self): def test_user_get_top_tags_with_limit(self):
# Arrange # Arrange
@ -489,15 +494,3 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
self.helper_validate_results(result1, result2, result3) self.helper_validate_results(result1, result2, result3)
def test_get_artist_tracks_deprecated(self):
# Arrange
lastfm_user = self.network.get_user(self.username)
# Act / Assert
with warnings.catch_warnings(), pytest.raises(
pylast.WSError,
match="Deprecated - This type of request is no longer supported",
):
warnings.filterwarnings("ignore", category=DeprecationWarning)
lastfm_user.get_artist_tracks(artist="Test Artist", stream=False)

View file

@ -1,8 +1,9 @@
from unittest import mock from unittest import mock
import pylast
import pytest import pytest
import pylast
def mock_network(): def mock_network():
return mock.Mock(_get_ws_auth=mock.Mock(return_value=("", "", ""))) return mock.Mock(_get_ws_auth=mock.Mock(return_value=("", "", "")))

34
tox.ini
View file

@ -1,21 +1,29 @@
[tox] [tox]
envlist = py{36, 37, 38, 39, 310, py3} envlist =
py{py3, 310, 39, 38, 37, 36}
[testenv] [testenv]
extras = tests passenv =
setenv = PYLAST_API_KEY
PYLAST_USERNAME={env:PYLAST_USERNAME:} PYLAST_API_SECRET
PYLAST_PASSWORD_HASH={env:PYLAST_PASSWORD_HASH:} PYLAST_PASSWORD_HASH
PYLAST_API_KEY={env:PYLAST_API_KEY:} PYLAST_USERNAME
PYLAST_API_SECRET={env:PYLAST_API_SECRET:} extras =
commands = pytest -v -s -W all --cov pylast --cov-report term-missing --random-order {posargs} tests
commands =
pytest -v -s -W all --cov pylast --cov tests --cov-report term-missing --random-order {posargs}
[testenv:venv] [testenv:venv]
deps = ipdb deps =
commands = {posargs} ipdb
commands =
{posargs}
[testenv:lint] [testenv:lint]
deps = pre-commit passenv =
commands = pre-commit run --all-files --show-diff-on-failure PRE_COMMIT_COLOR
skip_install = true skip_install = true
passenv = PRE_COMMIT_COLOR deps =
pre-commit
commands =
pre-commit run --all-files --show-diff-on-failure