Compare commits
No commits in common. "main" and "4.2.1" have entirely different histories.
|
@ -11,6 +11,7 @@ charset = utf-8
|
||||||
[*.py]
|
[*.py]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
# Two-space indentation
|
# Two-space indentation
|
||||||
|
|
3
.github/labels.yml
vendored
3
.github/labels.yml
vendored
|
@ -91,6 +91,9 @@
|
||||||
- color: b60205
|
- color: b60205
|
||||||
description: Removal of a feature, usually done in major releases
|
description: Removal of a feature, usually done in major releases
|
||||||
name: removal
|
name: removal
|
||||||
|
- color: 2d18b2
|
||||||
|
description: "To automatically merge PRs that are ready"
|
||||||
|
name: automerge
|
||||||
- color: 0366d6
|
- color: 0366d6
|
||||||
description: "For dependencies"
|
description: "For dependencies"
|
||||||
name: dependencies
|
name: dependencies
|
||||||
|
|
6
.github/release-drafter.yml
vendored
6
.github/release-drafter.yml
vendored
|
@ -22,12 +22,8 @@ categories:
|
||||||
exclude-labels:
|
exclude-labels:
|
||||||
- "changelog: skip"
|
- "changelog: skip"
|
||||||
|
|
||||||
autolabeler:
|
|
||||||
- label: "changelog: skip"
|
|
||||||
branch:
|
|
||||||
- "/pre-commit-ci-update-config/"
|
|
||||||
|
|
||||||
template: |
|
template: |
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
|
|
||||||
version-resolver:
|
version-resolver:
|
||||||
|
|
13
.github/renovate.json
vendored
13
.github/renovate.json
vendored
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": ["config:base"],
|
|
||||||
"labels": ["changelog: skip", "dependencies"],
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"groupName": "github-actions",
|
|
||||||
"matchManagers": ["github-actions"],
|
|
||||||
"separateMajorMinor": "false"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"schedule": ["on the first day of the month"]
|
|
||||||
}
|
|
88
.github/workflows/deploy.yml
vendored
88
.github/workflows/deploy.yml
vendored
|
@ -2,74 +2,56 @@ name: Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches:
|
||||||
tags: ["*"]
|
- master
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Always build & lint package.
|
build:
|
||||||
build-package:
|
if: github.repository == 'pylast/pylast'
|
||||||
name: Build & verify package
|
runs-on: ubuntu-20.04
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: hynek/build-and-inspect-python-package@v2
|
- name: Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
# Upload to Test PyPI on every commit on main.
|
|
||||||
release-test-pypi:
|
|
||||||
name: Publish in-dev package to test.pypi.org
|
|
||||||
if: |
|
|
||||||
github.repository_owner == 'pylast'
|
|
||||||
&& github.event_name == 'push'
|
|
||||||
&& github.ref == 'refs/heads/main'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build-package
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Download packages built by build-and-inspect-python-package
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: Packages
|
path: ~/.cache/pip
|
||||||
path: dist
|
key: deploy-${{ hashFiles('**/setup.py') }}
|
||||||
|
restore-keys: |
|
||||||
|
deploy-
|
||||||
|
|
||||||
- name: Upload package to Test PyPI
|
- name: Set up Python
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
repository-url: https://test.pypi.org/legacy/
|
python-version: 3.9
|
||||||
|
|
||||||
# Upload to real PyPI on GitHub Releases.
|
- name: Install dependencies
|
||||||
release-pypi:
|
run: |
|
||||||
name: Publish released package to pypi.org
|
python -m pip install -U pip
|
||||||
if: |
|
python -m pip install -U setuptools twine wheel
|
||||||
github.repository_owner == 'pylast'
|
|
||||||
&& github.event.action == 'published'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build-package
|
|
||||||
|
|
||||||
permissions:
|
- name: Build package
|
||||||
id-token: write
|
run: |
|
||||||
|
python setup.py --version
|
||||||
|
python setup.py sdist --format=gztar bdist_wheel
|
||||||
|
twine check dist/*
|
||||||
|
|
||||||
steps:
|
- name: Publish package to PyPI
|
||||||
- name: Download packages built by build-and-inspect-python-package
|
if: github.event.action == 'published'
|
||||||
uses: actions/download-artifact@v4
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
with:
|
with:
|
||||||
name: Packages
|
user: __token__
|
||||||
path: dist
|
password: ${{ secrets.pypi_password }}
|
||||||
|
|
||||||
- name: Upload package to PyPI
|
- name: Publish package to TestPyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
|
with:
|
||||||
|
user: __token__
|
||||||
|
password: ${{ secrets.test_pypi_password }}
|
||||||
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
|
12
.github/workflows/labels.yml
vendored
12
.github/workflows/labels.yml
vendored
|
@ -1,21 +1,15 @@
|
||||||
name: Sync labels
|
name: Sync labels
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- master
|
||||||
paths:
|
paths:
|
||||||
- .github/labels.yml
|
- .github/labels.yml
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: micnncim/action-label-syncer@v1
|
- uses: micnncim/action-label-syncer@v1
|
||||||
with:
|
with:
|
||||||
prune: false
|
prune: false
|
||||||
|
|
20
.github/workflows/lint.yml
vendored
20
.github/workflows/lint.yml
vendored
|
@ -1,22 +1,12 @@
|
||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on: [push, pull_request]
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_COLOR: 1
|
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v2
|
||||||
with:
|
- uses: pre-commit/action@v2.0.0
|
||||||
python-version: "3.x"
|
|
||||||
cache: pip
|
|
||||||
- uses: pre-commit/action@v3.0.1
|
|
||||||
|
|
25
.github/workflows/release-drafter.yml
vendored
25
.github/workflows/release-drafter.yml
vendored
|
@ -4,31 +4,14 @@ on:
|
||||||
push:
|
push:
|
||||||
# branches to consider in the event; optional, defaults to all
|
# branches to consider in the event; optional, defaults to all
|
||||||
branches:
|
branches:
|
||||||
- main
|
- master
|
||||||
# pull_request event is required only for autolabeler
|
|
||||||
pull_request:
|
|
||||||
# Only following types are handled by the action, but one can default to all as well
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
# pull_request_target event is required for autolabeler to support PRs from forks
|
|
||||||
# pull_request_target:
|
|
||||||
# types: [opened, reopened, synchronize]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update_release_draft:
|
update_release_draft:
|
||||||
if: github.repository_owner == 'pylast'
|
if: github.repository == 'pylast/pylast'
|
||||||
permissions:
|
|
||||||
# write permission is required to create a GitHub Release
|
|
||||||
contents: write
|
|
||||||
# write permission is required for autolabeler
|
|
||||||
# otherwise, read permission is required at least
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Drafts your next release notes as pull requests are merged into "main"
|
# Drafts your next release notes as pull requests are merged into "master"
|
||||||
- uses: release-drafter/release-drafter@v6
|
- uses: release-drafter/release-drafter@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
22
.github/workflows/require-pr-label.yml
vendored
22
.github/workflows/require-pr-label.yml
vendored
|
@ -1,22 +0,0 @@
|
||||||
name: Require PR label
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, labeled, unlabeled, synchronize]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
label:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: mheap/github-action-required-labels@v5
|
|
||||||
with:
|
|
||||||
mode: minimum
|
|
||||||
count: 1
|
|
||||||
labels:
|
|
||||||
"changelog: Added, changelog: Changed, changelog: Deprecated, changelog:
|
|
||||||
Fixed, changelog: Removed, changelog: Security, changelog: skip"
|
|
44
.github/workflows/test.yml
vendored
44
.github/workflows/test.yml
vendored
|
@ -1,28 +1,44 @@
|
||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on: [push, pull_request, workflow_dispatch]
|
on: [push, pull_request]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_COLOR: 1
|
FORCE_COLOR: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["pypy3.10", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev", "pypy3"]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-20.04]
|
||||||
|
include:
|
||||||
|
# Include new variables for Codecov
|
||||||
|
- { codecov-flag: GHA_Ubuntu2004, os: ubuntu-20.04 }
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
allow-prereleases: true
|
|
||||||
cache: pip
|
- 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
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
@ -40,15 +56,7 @@ jobs:
|
||||||
PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }}
|
PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }}
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v3.1.5
|
uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
flags: ${{ matrix.os }}
|
flags: ${{ matrix.codecov-flag }}
|
||||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||||||
|
|
||||||
success:
|
|
||||||
needs: test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Test successful
|
|
||||||
steps:
|
|
||||||
- name: Success
|
|
||||||
run: echo Test successful
|
|
||||||
|
|
8
.mergify.yml
Normal file
8
.mergify.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
pull_request_rules:
|
||||||
|
- name: Automatic merge on approval
|
||||||
|
conditions:
|
||||||
|
- label=automerge
|
||||||
|
- status-success=build
|
||||||
|
actions:
|
||||||
|
merge:
|
||||||
|
method: merge
|
|
@ -1,74 +1,49 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v0.5.0
|
rev: v2.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: pyupgrade
|
||||||
args: [--exit-non-zero-on-fix]
|
args: ["--py36-plus"]
|
||||||
|
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black
|
||||||
rev: 24.4.2
|
rev: 20.8b1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
args: ["--target-version", "py36"]
|
||||||
|
# override until resolved: https://github.com/psf/black/issues/402
|
||||||
|
files: \.pyi?$
|
||||||
|
types: []
|
||||||
|
|
||||||
- repo: https://github.com/asottile/blacken-docs
|
- repo: https://github.com/asottile/blacken-docs
|
||||||
rev: 1.18.0
|
rev: v1.9.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
args: [--target-version=py38]
|
args: ["--target-version", "py36"]
|
||||||
additional_dependencies: [black]
|
additional_dependencies: [black==20.8b1]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: v4.6.0
|
rev: 5.7.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-added-large-files
|
- id: isort
|
||||||
- id: check-case-conflict
|
|
||||||
- id: check-merge-conflict
|
|
||||||
- id: check-json
|
|
||||||
- id: check-toml
|
|
||||||
- id: check-yaml
|
|
||||||
- id: debug-statements
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: forbid-submodules
|
|
||||||
- id: trailing-whitespace
|
|
||||||
exclude: .github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE).md
|
|
||||||
|
|
||||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
rev: 0.28.6
|
rev: 3.8.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-github-workflows
|
- id: flake8
|
||||||
- id: check-renovate
|
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
|
||||||
|
|
||||||
- repo: https://github.com/rhysd/actionlint
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
rev: v1.7.1
|
rev: v1.7.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: actionlint
|
- id: python-check-blanket-noqa
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: 2.1.3
|
rev: v3.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: check-merge-conflict
|
||||||
|
- id: check-yaml
|
||||||
- repo: https://github.com/abravalheri/validate-pyproject
|
|
||||||
rev: v0.18
|
|
||||||
hooks:
|
|
||||||
- id: validate-pyproject
|
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/tox-ini-fmt
|
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||||
rev: 1.3.1
|
rev: 0.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: tox-ini-fmt
|
- id: tox-ini-fmt
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
|
||||||
rev: v4.0.0-alpha.8
|
|
||||||
hooks:
|
|
||||||
- id: prettier
|
|
||||||
args: [--prose-wrap=always, --print-width=88]
|
|
||||||
exclude: .github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE).md
|
|
||||||
|
|
||||||
- repo: meta
|
|
||||||
hooks:
|
|
||||||
- id: check-hooks-apply
|
|
||||||
- id: check-useless-excludes
|
|
||||||
|
|
||||||
ci:
|
|
||||||
autoupdate_schedule: quarterly
|
|
||||||
|
|
9
.scrutinizer.yml
Normal file
9
.scrutinizer.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
checks:
|
||||||
|
python:
|
||||||
|
code_rating: true
|
||||||
|
duplicate_code: true
|
||||||
|
filter:
|
||||||
|
excluded_paths:
|
||||||
|
- '*/test/*'
|
||||||
|
tools:
|
||||||
|
external_code_coverage: true
|
96
CHANGELOG.md
96
CHANGELOG.md
|
@ -12,125 +12,117 @@ See GitHub Releases:
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
- Fix unsafe creation of temp file for caching, and improve exception raising (#356)
|
* Fix unsafe creation of temp file for caching, and improve exception raising (#356) @kvanzuijlen
|
||||||
@kvanzuijlen
|
* [pre-commit.ci] pre-commit autoupdate (#362) @pre-commit-ci
|
||||||
- [pre-commit.ci] pre-commit autoupdate (#362) @pre-commit-ci
|
|
||||||
|
|
||||||
## [4.1.0] - 2021-01-04
|
## [4.1.0] - 2021-01-04
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
- Add support for streaming (#336) @kvanzuijlen
|
* Add support for streaming (#336) @kvanzuijlen
|
||||||
- Add Python 3.9 final to Travis CI (#350) @sheetalsingala
|
* Add Python 3.9 final to Travis CI (#350) @sheetalsingala
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
- Update copyright year (#360) @hugovk
|
* Update copyright year (#360) @hugovk
|
||||||
- Replace Travis CI with GitHub Actions (#352) @hugovk
|
* Replace Travis CI with GitHub Actions (#352) @hugovk
|
||||||
- [pre-commit.ci] pre-commit autoupdate (#359) @pre-commit-ci
|
* [pre-commit.ci] pre-commit autoupdate (#359) @pre-commit-ci
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
- Set limit to 50 by default, not 1 (#355) @hugovk
|
* Set limit to 50 by default, not 1 (#355) @hugovk
|
||||||
|
|
||||||
|
|
||||||
## [4.0.0] - 2020-10-07
|
## [4.0.0] - 2020-10-07
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
- Add support for Python 3.9 (#347) @hugovk
|
* Add support for Python 3.9 (#347) @hugovk
|
||||||
|
|
||||||
## Removed
|
## Removed
|
||||||
|
|
||||||
- Remove deprecated `Artist.get_cover_image`, `User.get_artist_tracks` and
|
* Remove deprecated `Artist.get_cover_image`, `User.get_artist_tracks` and `STATUS_TOKEN_ERROR` (#348) @hugovk
|
||||||
`STATUS_TOKEN_ERROR` (#348) @hugovk
|
* Drop support for EOL Python 3.5 (#346) @hugovk
|
||||||
- Drop support for EOL Python 3.5 (#346) @hugovk
|
|
||||||
|
|
||||||
## [3.3.0] - 2020-06-25
|
## [3.3.0] - 2020-06-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `User.get_now_playing`: Add album and cover image to info (#330) @hugovk
|
* `User.get_now_playing`: Add album and cover image to info (#330) @hugovk
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improve handling of error responses from the API (#327) @spiritualized
|
* Improve handling of error responses from the API (#327) @spiritualized
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
- Deprecate `Artist.get_cover_image`, they're no longer available from Last.fm (#332)
|
* Deprecate `Artist.get_cover_image`, they're no longer available from Last.fm (#332) @hugovk
|
||||||
@hugovk
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix `artist.get_bio_content()` to return `None` if bio is empty (#326) @hugovk
|
* Fix `artist.get_bio_content()` to return `None` if bio is empty (#326) @hugovk
|
||||||
|
|
||||||
|
|
||||||
## [3.2.1] - 2020-03-05
|
## [3.2.1] - 2020-03-05
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Only Python 3 is supported: don't create universal wheel (#318) @hugovk
|
* Only Python 3 is supported: don't create universal wheel (#318) @hugovk
|
||||||
- Fix regression calling `get_recent_tracks` with `limit=None` (#320) @hugovk
|
* Fix regression calling `get_recent_tracks` with `limit=None` (#320) @hugovk
|
||||||
- Fix `DeprecationWarning`: Please use `assertRegex` instead (#323) @hugovk
|
* Fix `DeprecationWarning`: Please use `assertRegex` instead (#323) @hugovk
|
||||||
|
|
||||||
## [3.2.0] - 2020-01-03
|
## [3.2.0] - 2020-01-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Support for Python 3.8
|
* Support for Python 3.8
|
||||||
- Store album art URLs when you call `GetTopAlbums` ([#307])
|
* Store album art URLs when you call `GetTopAlbums` ([#307])
|
||||||
- Retry paging through results on exception ([#297])
|
* Retry paging through results on exception ([#297])
|
||||||
- More error status codes from https://last.fm/api/errorcodes ([#297])
|
* More error status codes from https://last.fm/api/errorcodes ([#297])
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Respect `get_recent_tracks`' limit when there's a now playing track ([#310])
|
* Respect `get_recent_tracks`' limit when there's a now playing track ([#310])
|
||||||
- Move installable code to `src/` ([#301])
|
* Move installable code to `src/` ([#301])
|
||||||
- Update `get_weekly_artist_charts` docstring: only for `User` ([#311])
|
* Update `get_weekly_artist_charts` docstring: only for `User` ([#311])
|
||||||
- Remove Python 2 warnings, `python_requires` should be enough ([#312])
|
* Remove Python 2 warnings, `python_requires` should be enough ([#312])
|
||||||
- Use setuptools_scm to simplify versioning during release ([#316])
|
* Use setuptools_scm to simplify versioning during release ([#316])
|
||||||
- Various lint and test updates
|
* Various lint and test updates
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
- Last.fm's `user.getArtistTracks` has now been deprecated by Last.fm and is no longer
|
* Last.fm's `user.getArtistTracks` has now been deprecated by Last.fm and is no longer
|
||||||
available. Last.fm returns a "Deprecated - This type of request is no longer
|
available. Last.fm returns a "Deprecated - This type of request is no longer
|
||||||
supported" error when calling it. A future version of pylast will remove its
|
supported" error when calling it. A future version of pylast will remove its
|
||||||
`User.get_artist_tracks` altogether. ([#305])
|
`User.get_artist_tracks` altogether. ([#305])
|
||||||
|
|
||||||
- `STATUS_TOKEN_ERROR` is deprecated and will be removed in a future version. Use
|
* `STATUS_TOKEN_ERROR` is deprecated and will be removed in a future version.
|
||||||
`STATUS_OPERATION_FAILED` instead.
|
Use `STATUS_OPERATION_FAILED` instead.
|
||||||
|
|
||||||
## [3.1.0] - 2019-03-07
|
## [3.1.0] - 2019-03-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Extract username from session via new
|
* Extract username from session via new
|
||||||
`SessionKeyGenerator.get_web_auth_session_key_username` ([#290])
|
`SessionKeyGenerator.get_web_auth_session_key_username` ([#290])
|
||||||
- `User.get_track_scrobbles` ([#298])
|
* `User.get_track_scrobbles` ([#298])
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
- `User.get_artist_tracks`. Use `User.get_track_scrobbles` as a partial replacement.
|
* `User.get_artist_tracks`. Use `User.get_track_scrobbles` as a partial replacement.
|
||||||
([#298])
|
([#298])
|
||||||
|
|
||||||
## [3.0.0] - 2019-01-01
|
## [3.0.0] - 2019-01-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
* This changelog file ([#273])
|
||||||
- This changelog file ([#273])
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- Support for Python 2.7 ([#265])
|
* Support for Python 2.7 ([#265])
|
||||||
|
|
||||||
- Constants `COVER_SMALL`, `COVER_MEDIUM`, `COVER_LARGE`, `COVER_EXTRA_LARGE` and
|
* Constants `COVER_SMALL`, `COVER_MEDIUM`, `COVER_LARGE`, `COVER_EXTRA_LARGE`
|
||||||
`COVER_MEGA`. Use `SIZE_SMALL` etc. instead. ([#282])
|
and `COVER_MEGA`. Use `SIZE_SMALL` etc. instead. ([#282])
|
||||||
|
|
||||||
## [2.4.0] - 2018-08-08
|
## [2.4.0] - 2018-08-08
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
- Support for Python 2.7 ([#265])
|
* Support for Python 2.7 ([#265])
|
||||||
|
|
||||||
[4.2.0]: https://github.com/pylast/pylast/compare/4.1.0...4.2.0
|
[4.2.0]: https://github.com/pylast/pylast/compare/4.1.0...4.2.0
|
||||||
[4.1.0]: https://github.com/pylast/pylast/compare/4.0.0...4.1.0
|
[4.1.0]: https://github.com/pylast/pylast/compare/4.0.0...4.1.0
|
||||||
|
|
6
COPYING
6
COPYING
|
@ -32,11 +32,11 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||||
|
|
||||||
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||||
|
|
||||||
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||||
|
|
||||||
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||||
|
|
||||||
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||||
|
|
127
README.md
127
README.md
|
@ -1,11 +1,12 @@
|
||||||
# pyLast
|
pyLast
|
||||||
|
======
|
||||||
|
|
||||||
[](https://pypi.org/project/pylast/)
|
[](https://pypi.org/project/pylast/)
|
||||||
[](https://pypi.org/project/pylast/)
|
[](https://pypi.org/project/pylast/)
|
||||||
[](https://pypistats.org/packages/pylast)
|
[](https://pypistats.org/packages/pylast)
|
||||||
[](https://github.com/pylast/pylast/actions)
|
[](https://github.com/pylast/pylast/actions)
|
||||||
[](https://codecov.io/gh/pylast/pylast)
|
[](https://codecov.io/gh/pylast/pylast)
|
||||||
[](https://github.com/psf/black)
|
[](https://github.com/psf/black)
|
||||||
[](https://zenodo.org/badge/latestdoi/7803088)
|
[](https://zenodo.org/badge/latestdoi/7803088)
|
||||||
|
|
||||||
A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites
|
A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites
|
||||||
|
@ -13,48 +14,53 @@ such as [Libre.fm](https://libre.fm/).
|
||||||
|
|
||||||
Use the pydoc utility for help on usage or see [tests/](tests/) for examples.
|
Use the pydoc utility for help on usage or see [tests/](tests/) for examples.
|
||||||
|
|
||||||
## Installation
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Install via pip:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -m pip install pylast
|
||||||
|
```
|
||||||
|
|
||||||
Install latest development version:
|
Install latest development version:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 -m pip install -U git+https://git.hirad.it/Hirad/pylast
|
python3 -m pip install -U git+https://github.com/pylast/pylast
|
||||||
```
|
```
|
||||||
|
|
||||||
Or from requirements.txt:
|
Or from requirements.txt:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
-e https://git.hirad.it/Hirad/pylast#egg=pylast
|
-e git://github.com/pylast/pylast.git#egg=pylast
|
||||||
```
|
```
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
|
|
||||||
- pyLast 5.3+ supports Python 3.8-3.13.
|
* pyLast 4.0+ supports Python 3.6-3.9.
|
||||||
- pyLast 5.2+ supports Python 3.8-3.12.
|
* pyLast 3.2 - 3.3 supports Python 3.5-3.8.
|
||||||
- pyLast 5.1 supports Python 3.7-3.11.
|
* pyLast 3.0 - 3.1 supports Python 3.5-3.7.
|
||||||
- pyLast 5.0 supports Python 3.7-3.10.
|
* pyLast 2.2 - 2.4 supports Python 2.7.10+, 3.4-3.7.
|
||||||
- pyLast 4.3 - 4.5 supports Python 3.6-3.10.
|
* pyLast 2.0 - 2.1 supports Python 2.7.10+, 3.4-3.6.
|
||||||
- pyLast 4.0 - 4.2 supports Python 3.6-3.9.
|
* pyLast 1.7 - 1.9 supports Python 2.7, 3.3-3.6.
|
||||||
- pyLast 3.2 - 3.3 supports Python 3.5-3.8.
|
* pyLast 1.0 - 1.6 supports Python 2.7, 3.3-3.4.
|
||||||
- pyLast 3.0 - 3.1 supports Python 3.5-3.7.
|
* pyLast 0.5 supports Python 2, 3.
|
||||||
- pyLast 2.2 - 2.4 supports Python 2.7.10+, 3.4-3.7.
|
* pyLast < 0.5 supports Python 2.
|
||||||
- pyLast 2.0 - 2.1 supports Python 2.7.10+, 3.4-3.6.
|
|
||||||
- pyLast 1.7 - 1.9 supports Python 2.7, 3.3-3.6.
|
|
||||||
- pyLast 1.0 - 1.6 supports Python 2.7, 3.3-3.4.
|
|
||||||
- pyLast 0.5 supports Python 2, 3.
|
|
||||||
- pyLast < 0.5 supports Python 2.
|
|
||||||
|
|
||||||
## Features
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
- Simple public interface.
|
* Simple public interface.
|
||||||
- Access to all the data exposed by the Last.fm web services.
|
* Access to all the data exposed by the Last.fm web services.
|
||||||
- Scrobbling support.
|
* Scrobbling support.
|
||||||
- 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).
|
||||||
- Support for other API-compatible networks like Libre.fm.
|
* Support for other API-compatible networks like Libre.fm.
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
Getting started
|
||||||
|
---------------
|
||||||
|
|
||||||
Here's some simple code example to get you started. In order to create any object from
|
Here's some simple code example to get you started. In order to create any object from
|
||||||
pyLast, you need a `Network` object which represents a social music network that is
|
pyLast, you need a `Network` object which represents a social music network that is
|
||||||
|
@ -79,44 +85,12 @@ network = pylast.LastFMNetwork(
|
||||||
username=username,
|
username=username,
|
||||||
password_hash=password_hash,
|
password_hash=password_hash,
|
||||||
)
|
)
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, instead of creating `network` with a username and password, you can
|
|
||||||
authenticate with a session key:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import pylast
|
|
||||||
|
|
||||||
SESSION_KEY_FILE = os.path.join(os.path.expanduser("~"), ".session_key")
|
|
||||||
network = pylast.LastFMNetwork(API_KEY, API_SECRET)
|
|
||||||
if not os.path.exists(SESSION_KEY_FILE):
|
|
||||||
skg = pylast.SessionKeyGenerator(network)
|
|
||||||
url = skg.get_web_auth_url()
|
|
||||||
|
|
||||||
print(f"Please authorize this script to access your account: {url}\n")
|
|
||||||
import time
|
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
webbrowser.open(url)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
session_key = skg.get_web_auth_session_key(url)
|
|
||||||
with open(SESSION_KEY_FILE, "w") as f:
|
|
||||||
f.write(session_key)
|
|
||||||
break
|
|
||||||
except pylast.WSError:
|
|
||||||
time.sleep(1)
|
|
||||||
else:
|
|
||||||
session_key = open(SESSION_KEY_FILE).read()
|
|
||||||
|
|
||||||
network.session_key = session_key
|
|
||||||
```
|
|
||||||
|
|
||||||
And away we go:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Now you can use that object everywhere
|
# Now you can use that object everywhere
|
||||||
|
artist = network.get_artist("System of a Down")
|
||||||
|
artist.shout("<3")
|
||||||
|
|
||||||
|
|
||||||
track = network.get_track("Iron Maiden", "The Nomad")
|
track = network.get_track("Iron Maiden", "The Nomad")
|
||||||
track.love()
|
track.love()
|
||||||
track.add_tags(("awesome", "favorite"))
|
track.add_tags(("awesome", "favorite"))
|
||||||
|
@ -127,18 +101,18 @@ track.add_tags(("awesome", "favorite"))
|
||||||
|
|
||||||
More examples in
|
More examples in
|
||||||
<a href="https://github.com/hugovk/lastfm-tools">hugovk/lastfm-tools</a> and
|
<a href="https://github.com/hugovk/lastfm-tools">hugovk/lastfm-tools</a> and
|
||||||
[tests/](https://github.com/pylast/pylast/tree/main/tests).
|
[tests/](https://github.com/pylast/pylast/tree/master/tests).
|
||||||
|
|
||||||
## Testing
|
Testing
|
||||||
|
-------
|
||||||
|
|
||||||
The [tests/](https://github.com/pylast/pylast/tree/main/tests) directory contains
|
The [tests/](https://github.com/pylast/pylast/tree/master/tests) directory contains
|
||||||
integration and unit tests with Last.fm, and plenty of code examples.
|
integration and unit tests with Last.fm, and plenty of code examples.
|
||||||
|
|
||||||
For integration tests you need a test account at Last.fm that will become cluttered with
|
For integration tests you need a test account at Last.fm that will become cluttered with
|
||||||
test data, and an API key and secret. Either copy
|
test data, and an API key and secret. Either copy
|
||||||
[example_test_pylast.yaml](https://github.com/pylast/pylast/blob/main/example_test_pylast.yaml)
|
[example_test_pylast.yaml](example_test_pylast.yaml) to test_pylast.yaml and fill out
|
||||||
to test_pylast.yaml and fill out the credentials, or set them as environment variables
|
the credentials, or set them as environment variables like:
|
||||||
like:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE
|
export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE
|
||||||
|
@ -169,7 +143,8 @@ coverage html # for HTML report
|
||||||
open htmlcov/index.html
|
open htmlcov/index.html
|
||||||
```
|
```
|
||||||
|
|
||||||
## Logging
|
Logging
|
||||||
|
-------
|
||||||
|
|
||||||
To enable from your own code:
|
To enable from your own code:
|
||||||
|
|
||||||
|
@ -177,8 +152,7 @@ To enable from your own code:
|
||||||
import logging
|
import logging
|
||||||
import pylast
|
import pylast
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
network = pylast.LastFMNetwork(...)
|
network = pylast.LastFMNetwork(...)
|
||||||
```
|
```
|
||||||
|
@ -186,8 +160,5 @@ network = pylast.LastFMNetwork(...)
|
||||||
To enable from pytest:
|
To enable from pytest:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pytest --log-cli-level info -k test_album_search_images
|
pytest --log-cli-level debug -k test_album_search_images
|
||||||
```
|
```
|
||||||
|
|
||||||
To also see data returned from the API, use `level=logging.DEBUG` or
|
|
||||||
`--log-cli-level debug` instead.
|
|
||||||
|
|
17
RELEASING.md
17
RELEASING.md
|
@ -1,22 +1,21 @@
|
||||||
# Release Checklist
|
# Release Checklist
|
||||||
|
|
||||||
- [ ] Get `main` to the appropriate code release state.
|
* [ ] Get master to the appropriate code release state.
|
||||||
[GitHub Actions](https://github.com/pylast/pylast/actions) should be running
|
[GitHub Actions](https://github.com/pylast/pylast/actions) should be running cleanly for
|
||||||
cleanly for all merges to `main`.
|
all merges to master.
|
||||||
[](https://github.com/pylast/pylast/actions)
|
[](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
|
||||||
|
|
||||||
- [ ] Check next tag is correct, amend if needed
|
* [ ] Check next tag is correct, amend if needed
|
||||||
|
|
||||||
- [ ] Publish release
|
* [ ] Publish release
|
||||||
|
|
||||||
- [ ] Check the tagged
|
* [ ] Check the tagged [GitHub Actions build](https://github.com/pylast/pylast/actions?query=workflow%3ADeploy)
|
||||||
[GitHub Actions build](https://github.com/pylast/pylast/actions/workflows/deploy.yml)
|
|
||||||
has deployed to [PyPI](https://pypi.org/project/pylast/#history)
|
has deployed to [PyPI](https://pypi.org/project/pylast/#history)
|
||||||
|
|
||||||
- [ ] Check installation:
|
* [ ] Check installation:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip3 uninstall -y pylast && pip3 install -U pylast && python3 -c "import pylast; print(pylast.__version__)"
|
pip3 uninstall -y pylast && pip3 install -U pylast && python3 -c "import pylast; print(pylast.__version__)"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
username: TODO_ENTER_YOURS_HERE
|
username: TODO_ENTER_YOURS_HERE
|
||||||
password_hash: TODO_ENTER_YOURS_HERE
|
password_hash: TODO_ENTER_YOURS_HERE
|
||||||
api_key: TODO_ENTER_YOURS_HERE
|
api_key: TODO_ENTER_YOURS_HERE
|
||||||
api_secret: TODO_ENTER_YOURS_HERE
|
api_secret: TODO_ENTER_YOURS_HERE
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
[build-system]
|
|
||||||
build-backend = "hatchling.build"
|
|
||||||
requires = [
|
|
||||||
"hatch-vcs",
|
|
||||||
"hatchling",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "pylast"
|
|
||||||
description = "A Python interface to Last.fm and Libre.fm"
|
|
||||||
readme = "README.md"
|
|
||||||
keywords = [
|
|
||||||
"Last.fm",
|
|
||||||
"music",
|
|
||||||
"scrobble",
|
|
||||||
"scrobbling",
|
|
||||||
]
|
|
||||||
license = { text = "Apache-2.0" }
|
|
||||||
maintainers = [
|
|
||||||
{ name = "Hugo van Kemenade" },
|
|
||||||
]
|
|
||||||
authors = [
|
|
||||||
{ name = "Amr Hassan <amr.hassan@gmail.com> and Contributors", email = "amr.hassan@gmail.com" },
|
|
||||||
]
|
|
||||||
requires-python = ">=3.8"
|
|
||||||
classifiers = [
|
|
||||||
"Development Status :: 5 - Production/Stable",
|
|
||||||
"License :: OSI Approved :: Apache Software License",
|
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
|
||||||
"Programming Language :: Python :: 3.12",
|
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
|
||||||
"Topic :: Internet",
|
|
||||||
"Topic :: Multimedia :: Sound/Audio",
|
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
||||||
]
|
|
||||||
dynamic = [
|
|
||||||
"version",
|
|
||||||
]
|
|
||||||
dependencies = [
|
|
||||||
"httpx",
|
|
||||||
]
|
|
||||||
optional-dependencies.tests = [
|
|
||||||
"flaky",
|
|
||||||
"pytest",
|
|
||||||
"pytest-cov",
|
|
||||||
"pytest-random-order",
|
|
||||||
"pyyaml",
|
|
||||||
]
|
|
||||||
urls.Changelog = "https://github.com/pylast/pylast/releases"
|
|
||||||
urls.Homepage = "https://github.com/pylast/pylast"
|
|
||||||
urls.Source = "https://github.com/pylast/pylast"
|
|
||||||
|
|
||||||
[tool.hatch]
|
|
||||||
version.source = "vcs"
|
|
||||||
|
|
||||||
[tool.hatch.version.raw-options]
|
|
||||||
local_scheme = "no-local-version"
|
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
fix = true
|
|
||||||
|
|
||||||
lint.select = [
|
|
||||||
"C4", # flake8-comprehensions
|
|
||||||
"E", # pycodestyle errors
|
|
||||||
"EM", # flake8-errmsg
|
|
||||||
"F", # pyflakes errors
|
|
||||||
"I", # isort
|
|
||||||
"ISC", # flake8-implicit-str-concat
|
|
||||||
"LOG", # flake8-logging
|
|
||||||
"PGH", # pygrep-hooks
|
|
||||||
"RUF022", # unsorted-dunder-all
|
|
||||||
"RUF100", # unused noqa (yesqa)
|
|
||||||
"UP", # pyupgrade
|
|
||||||
"W", # pycodestyle warnings
|
|
||||||
"YTT", # flake8-2020
|
|
||||||
]
|
|
||||||
lint.extend-ignore = [
|
|
||||||
"E203", # Whitespace before ':'
|
|
||||||
"E221", # Multiple spaces before operator
|
|
||||||
"E226", # Missing whitespace around arithmetic operator
|
|
||||||
"E241", # Multiple spaces after ','
|
|
||||||
]
|
|
||||||
lint.isort.known-first-party = [
|
|
||||||
"pylast",
|
|
||||||
]
|
|
||||||
lint.isort.required-imports = [
|
|
||||||
"from __future__ import annotations",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.pyproject-fmt]
|
|
||||||
max_supported_python = "3.13"
|
|
|
@ -2,5 +2,3 @@
|
||||||
filterwarnings =
|
filterwarnings =
|
||||||
once::DeprecationWarning
|
once::DeprecationWarning
|
||||||
once::PendingDeprecationWarning
|
once::PendingDeprecationWarning
|
||||||
|
|
||||||
xfail_strict=true
|
|
||||||
|
|
5
setup.cfg
Normal file
5
setup.cfg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[flake8]
|
||||||
|
max_line_length = 88
|
||||||
|
|
||||||
|
[tool:isort]
|
||||||
|
profile = black
|
46
setup.py
Executable file
46
setup.py
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
with open("README.md") as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def local_scheme(version):
|
||||||
|
"""Skip the local version (eg. +xyz of 0.6.1.dev4+gdf99fe2)
|
||||||
|
to be able to upload to Test PyPI"""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="pylast",
|
||||||
|
description="A Python interface to Last.fm and Libre.fm",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
author="Amr Hassan <amr.hassan@gmail.com> and Contributors",
|
||||||
|
author_email="amr.hassan@gmail.com",
|
||||||
|
url="https://github.com/pylast/pylast",
|
||||||
|
license="Apache2",
|
||||||
|
keywords=["Last.fm", "music", "scrobble", "scrobbling"],
|
||||||
|
packages=find_packages(where="src"),
|
||||||
|
package_dir={"": "src"},
|
||||||
|
use_scm_version={"local_scheme": local_scheme},
|
||||||
|
setup_requires=["setuptools_scm"],
|
||||||
|
extras_require={
|
||||||
|
"tests": ["flaky", "pytest", "pytest-cov", "pytest-random-order", "pyyaml"]
|
||||||
|
},
|
||||||
|
python_requires=">=3.6",
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Topic :: Internet",
|
||||||
|
"Topic :: Multimedia :: Sound/Audio",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.6",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
],
|
||||||
|
)
|
File diff suppressed because it is too large
Load diff
|
@ -2,15 +2,13 @@
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pylast
|
import pylast
|
||||||
|
|
||||||
from .test_pylast import TestPyLastWithLastFm
|
from .test_pylast import TestPyLastWithLastFm
|
||||||
|
|
||||||
|
|
||||||
class TestPyLastAlbum(TestPyLastWithLastFm):
|
class TestPyLastAlbum(TestPyLastWithLastFm):
|
||||||
def test_album_tags_are_topitems(self) -> None:
|
def test_album_tags_are_topitems(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album = self.network.get_album("Test Artist", "Test Album")
|
album = self.network.get_album("Test Artist", "Test Album")
|
||||||
|
|
||||||
|
@ -21,14 +19,14 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
|
||||||
assert len(tags) > 0
|
assert len(tags) > 0
|
||||||
assert isinstance(tags[0], pylast.TopItem)
|
assert isinstance(tags[0], pylast.TopItem)
|
||||||
|
|
||||||
def test_album_is_hashable(self) -> None:
|
def test_album_is_hashable(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album = self.network.get_album("Test Artist", "Test Album")
|
album = self.network.get_album("Test Artist", "Test Album")
|
||||||
|
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
self.helper_is_thing_hashable(album)
|
self.helper_is_thing_hashable(album)
|
||||||
|
|
||||||
def test_album_in_recent_tracks(self) -> None:
|
def test_album_in_recent_tracks(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user(self.username)
|
lastfm_user = self.network.get_user(self.username)
|
||||||
|
|
||||||
|
@ -39,7 +37,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert hasattr(track, "album")
|
assert hasattr(track, "album")
|
||||||
|
|
||||||
def test_album_wiki_content(self) -> None:
|
def test_album_wiki_content(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album = pylast.Album("Test Artist", "Test Album", self.network)
|
album = pylast.Album("Test Artist", "Test Album", self.network)
|
||||||
|
|
||||||
|
@ -50,7 +48,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
|
||||||
assert wiki is not None
|
assert wiki is not None
|
||||||
assert len(wiki) >= 1
|
assert len(wiki) >= 1
|
||||||
|
|
||||||
def test_album_wiki_published_date(self) -> None:
|
def test_album_wiki_published_date(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album = pylast.Album("Test Artist", "Test Album", self.network)
|
album = pylast.Album("Test Artist", "Test Album", self.network)
|
||||||
|
|
||||||
|
@ -61,7 +59,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
|
||||||
assert wiki is not None
|
assert wiki is not None
|
||||||
assert len(wiki) >= 1
|
assert len(wiki) >= 1
|
||||||
|
|
||||||
def test_album_wiki_summary(self) -> None:
|
def test_album_wiki_summary(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album = pylast.Album("Test Artist", "Test Album", self.network)
|
album = pylast.Album("Test Artist", "Test Album", self.network)
|
||||||
|
|
||||||
|
@ -72,7 +70,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
|
||||||
assert wiki is not None
|
assert wiki is not None
|
||||||
assert len(wiki) >= 1
|
assert len(wiki) >= 1
|
||||||
|
|
||||||
def test_album_eq_none_is_false(self) -> None:
|
def test_album_eq_none_is_false(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album1 = None
|
album1 = None
|
||||||
album2 = pylast.Album("Test Artist", "Test Album", self.network)
|
album2 = pylast.Album("Test Artist", "Test Album", self.network)
|
||||||
|
@ -80,7 +78,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
assert album1 != album2
|
assert album1 != album2
|
||||||
|
|
||||||
def test_album_ne_none_is_true(self) -> None:
|
def test_album_ne_none_is_true(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album1 = None
|
album1 = None
|
||||||
album2 = pylast.Album("Test Artist", "Test Album", self.network)
|
album2 = pylast.Album("Test Artist", "Test Album", self.network)
|
||||||
|
@ -88,7 +86,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
assert album1 != album2
|
assert album1 != album2
|
||||||
|
|
||||||
def test_get_cover_image(self) -> None:
|
def test_get_cover_image(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album = self.network.get_album("Test Artist", "Test Album")
|
album = self.network.get_album("Test Artist", "Test Album")
|
||||||
|
|
||||||
|
@ -96,25 +94,5 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
|
||||||
image = album.get_cover_image()
|
image = album.get_cover_image()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert image.startswith("https://")
|
self.assert_startswith(image, "https://")
|
||||||
assert image.endswith(".gif") or image.endswith(".png")
|
self.assert_endswith(image, ".png")
|
||||||
|
|
||||||
def test_mbid(self) -> None:
|
|
||||||
# Arrange
|
|
||||||
album = self.network.get_album("Radiohead", "OK Computer")
|
|
||||||
|
|
||||||
# Act
|
|
||||||
mbid = album.get_mbid()
|
|
||||||
|
|
||||||
# Assert
|
|
||||||
assert mbid == "0b6b4ba0-d36f-47bd-b4ea-6a5b91842d29"
|
|
||||||
|
|
||||||
def test_no_mbid(self) -> None:
|
|
||||||
# Arrange
|
|
||||||
album = self.network.get_album("Test Artist", "Test Album")
|
|
||||||
|
|
||||||
# Act
|
|
||||||
mbid = album.get_mbid()
|
|
||||||
|
|
||||||
# Assert
|
|
||||||
assert mbid is None
|
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import pylast
|
import pylast
|
||||||
|
@ -12,7 +10,7 @@ from .test_pylast import WRITE_TEST, TestPyLastWithLastFm
|
||||||
|
|
||||||
|
|
||||||
class TestPyLastArtist(TestPyLastWithLastFm):
|
class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
def test_repr(self) -> None:
|
def test_repr(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = pylast.Artist("Test Artist", self.network)
|
artist = pylast.Artist("Test Artist", self.network)
|
||||||
|
|
||||||
|
@ -22,16 +20,16 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert representation.startswith("pylast.Artist('Test Artist',")
|
assert representation.startswith("pylast.Artist('Test Artist',")
|
||||||
|
|
||||||
def test_artist_is_hashable(self) -> None:
|
def test_artist_is_hashable(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
test_artist = self.network.get_artist("Radiohead")
|
test_artist = self.network.get_artist("Test Artist")
|
||||||
artist = test_artist.get_similar(limit=2)[0].item
|
artist = test_artist.get_similar(limit=2)[0].item
|
||||||
assert isinstance(artist, pylast.Artist)
|
assert isinstance(artist, pylast.Artist)
|
||||||
|
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
self.helper_is_thing_hashable(artist)
|
self.helper_is_thing_hashable(artist)
|
||||||
|
|
||||||
def test_bio_published_date(self) -> None:
|
def test_bio_published_date(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = pylast.Artist("Test Artist", self.network)
|
artist = pylast.Artist("Test Artist", self.network)
|
||||||
|
|
||||||
|
@ -42,7 +40,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
assert bio is not None
|
assert bio is not None
|
||||||
assert len(bio) >= 1
|
assert len(bio) >= 1
|
||||||
|
|
||||||
def test_bio_content(self) -> None:
|
def test_bio_content(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = pylast.Artist("Test Artist", self.network)
|
artist = pylast.Artist("Test Artist", self.network)
|
||||||
|
|
||||||
|
@ -53,7 +51,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
assert bio is not None
|
assert bio is not None
|
||||||
assert len(bio) >= 1
|
assert len(bio) >= 1
|
||||||
|
|
||||||
def test_bio_content_none(self) -> None:
|
def test_bio_content_none(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# An artist with no biography, with "<content/>" in the API XML
|
# An artist with no biography, with "<content/>" in the API XML
|
||||||
artist = pylast.Artist("Mr Sizef + Unquote", self.network)
|
artist = pylast.Artist("Mr Sizef + Unquote", self.network)
|
||||||
|
@ -64,7 +62,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert bio is None
|
assert bio is None
|
||||||
|
|
||||||
def test_bio_summary(self) -> None:
|
def test_bio_summary(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = pylast.Artist("Test Artist", self.network)
|
artist = pylast.Artist("Test Artist", self.network)
|
||||||
|
|
||||||
|
@ -75,7 +73,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
assert bio is not None
|
assert bio is not None
|
||||||
assert len(bio) >= 1
|
assert len(bio) >= 1
|
||||||
|
|
||||||
def test_artist_top_tracks(self) -> None:
|
def test_artist_top_tracks(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Pick an artist with plenty of plays
|
# Pick an artist with plenty of plays
|
||||||
artist = self.network.get_top_artists(limit=1)[0].item
|
artist = self.network.get_top_artists(limit=1)[0].item
|
||||||
|
@ -86,7 +84,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
||||||
|
|
||||||
def test_artist_top_albums(self) -> None:
|
def test_artist_top_albums(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Pick an artist with plenty of plays
|
# Pick an artist with plenty of plays
|
||||||
artist = self.network.get_top_artists(limit=1)[0].item
|
artist = self.network.get_top_artists(limit=1)[0].item
|
||||||
|
@ -109,7 +107,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert len(things) == test_limit
|
assert len(things) == test_limit
|
||||||
|
|
||||||
def test_artist_top_albums_limit_default(self) -> None:
|
def test_artist_top_albums_limit_default(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Pick an artist with plenty of plays
|
# Pick an artist with plenty of plays
|
||||||
artist = self.network.get_top_artists(limit=1)[0].item
|
artist = self.network.get_top_artists(limit=1)[0].item
|
||||||
|
@ -120,7 +118,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert len(things) == 50
|
assert len(things) == 50
|
||||||
|
|
||||||
def test_artist_listener_count(self) -> None:
|
def test_artist_listener_count(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = self.network.get_artist("Test Artist")
|
artist = self.network.get_artist("Test Artist")
|
||||||
|
|
||||||
|
@ -132,7 +130,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
assert count > 0
|
assert count > 0
|
||||||
|
|
||||||
@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")
|
||||||
def test_tag_artist(self) -> None:
|
def test_tag_artist(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = self.network.get_artist("Test Artist")
|
artist = self.network.get_artist("Test Artist")
|
||||||
# artist.clear_tags()
|
# artist.clear_tags()
|
||||||
|
@ -147,7 +145,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
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")
|
||||||
def test_remove_tag_of_type_text(self) -> None:
|
def test_remove_tag_of_type_text(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
tag = "testing" # text
|
tag = "testing" # text
|
||||||
artist = self.network.get_artist("Test Artist")
|
artist = self.network.get_artist("Test Artist")
|
||||||
|
@ -162,7 +160,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
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")
|
||||||
def test_remove_tag_of_type_tag(self) -> None:
|
def test_remove_tag_of_type_tag(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
tag = pylast.Tag("testing", self.network) # Tag
|
tag = pylast.Tag("testing", self.network) # Tag
|
||||||
artist = self.network.get_artist("Test Artist")
|
artist = self.network.get_artist("Test Artist")
|
||||||
|
@ -177,7 +175,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
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")
|
||||||
def test_remove_tags(self) -> None:
|
def test_remove_tags(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
tags = ["removetag1", "removetag2"]
|
tags = ["removetag1", "removetag2"]
|
||||||
artist = self.network.get_artist("Test Artist")
|
artist = self.network.get_artist("Test Artist")
|
||||||
|
@ -197,7 +195,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
assert not found2
|
assert not found2
|
||||||
|
|
||||||
@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")
|
||||||
def test_set_tags(self) -> None:
|
def test_set_tags(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
tags = ["sometag1", "sometag2"]
|
tags = ["sometag1", "sometag2"]
|
||||||
artist = self.network.get_artist("Test Artist 2")
|
artist = self.network.get_artist("Test Artist 2")
|
||||||
|
@ -221,7 +219,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
assert found1
|
assert found1
|
||||||
assert found2
|
assert found2
|
||||||
|
|
||||||
def test_artists(self) -> None:
|
def test_artists(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist1 = self.network.get_artist("Radiohead")
|
artist1 = self.network.get_artist("Radiohead")
|
||||||
artist2 = self.network.get_artist("Portishead")
|
artist2 = self.network.get_artist("Portishead")
|
||||||
|
@ -231,6 +229,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
mbid = artist1.get_mbid()
|
mbid = artist1.get_mbid()
|
||||||
|
|
||||||
playcount = artist1.get_playcount()
|
playcount = artist1.get_playcount()
|
||||||
|
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)
|
||||||
|
|
||||||
|
@ -240,8 +239,9 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
assert name.lower() == name_cap.lower()
|
assert name.lower() == name_cap.lower()
|
||||||
assert url == "https://www.last.fm/music/radiohead"
|
assert url == "https://www.last.fm/music/radiohead"
|
||||||
assert mbid == "a74b1b7f-71a5-4011-9441-d0b5e4122711"
|
assert mbid == "a74b1b7f-71a5-4011-9441-d0b5e4122711"
|
||||||
|
assert isinstance(streamable, bool)
|
||||||
|
|
||||||
def test_artist_eq_none_is_false(self) -> None:
|
def test_artist_eq_none_is_false(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist1 = None
|
artist1 = None
|
||||||
artist2 = pylast.Artist("Test Artist", self.network)
|
artist2 = pylast.Artist("Test Artist", self.network)
|
||||||
|
@ -249,7 +249,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
assert artist1 != artist2
|
assert artist1 != artist2
|
||||||
|
|
||||||
def test_artist_ne_none_is_true(self) -> None:
|
def test_artist_ne_none_is_true(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist1 = None
|
artist1 = None
|
||||||
artist2 = pylast.Artist("Test Artist", self.network)
|
artist2 = pylast.Artist("Test Artist", self.network)
|
||||||
|
@ -257,7 +257,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
assert artist1 != artist2
|
assert artist1 != artist2
|
||||||
|
|
||||||
def test_artist_get_correction(self) -> None:
|
def test_artist_get_correction(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = pylast.Artist("guns and roses", self.network)
|
artist = pylast.Artist("guns and roses", self.network)
|
||||||
|
|
||||||
|
@ -267,7 +267,8 @@ class TestPyLastArtist(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert corrected_artist_name == "Guns N' Roses"
|
assert corrected_artist_name == "Guns N' Roses"
|
||||||
|
|
||||||
def test_get_userplaycount(self) -> None:
|
@pytest.mark.xfail
|
||||||
|
def test_get_userplaycount(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = pylast.Artist("John Lennon", self.network, username=self.username)
|
artist = pylast.Artist("John Lennon", self.network, username=self.username)
|
||||||
|
|
||||||
|
@ -275,4 +276,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
|
||||||
|
|
|
@ -2,22 +2,20 @@
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pylast
|
import pylast
|
||||||
|
|
||||||
from .test_pylast import TestPyLastWithLastFm
|
from .test_pylast import TestPyLastWithLastFm
|
||||||
|
|
||||||
|
|
||||||
class TestPyLastCountry(TestPyLastWithLastFm):
|
class TestPyLastCountry(TestPyLastWithLastFm):
|
||||||
def test_country_is_hashable(self) -> None:
|
def test_country_is_hashable(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
country = self.network.get_country("Italy")
|
country = self.network.get_country("Italy")
|
||||||
|
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
self.helper_is_thing_hashable(country)
|
self.helper_is_thing_hashable(country)
|
||||||
|
|
||||||
def test_countries(self) -> None:
|
def test_countries(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
country1 = pylast.Country("Italy", self.network)
|
country1 = pylast.Country("Italy", self.network)
|
||||||
country2 = pylast.Country("Finland", self.network)
|
country2 = pylast.Country("Finland", self.network)
|
||||||
|
|
|
@ -2,15 +2,13 @@
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pylast
|
import pylast
|
||||||
|
|
||||||
from .test_pylast import TestPyLastWithLastFm
|
from .test_pylast import TestPyLastWithLastFm
|
||||||
|
|
||||||
|
|
||||||
class TestPyLastLibrary(TestPyLastWithLastFm):
|
class TestPyLastLibrary(TestPyLastWithLastFm):
|
||||||
def test_repr(self) -> None:
|
def test_repr(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
library = pylast.Library(user=self.username, network=self.network)
|
library = pylast.Library(user=self.username, network=self.network)
|
||||||
|
|
||||||
|
@ -18,9 +16,9 @@ class TestPyLastLibrary(TestPyLastWithLastFm):
|
||||||
representation = repr(library)
|
representation = repr(library)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert representation.startswith("pylast.Library(")
|
self.assert_startswith(representation, "pylast.Library(")
|
||||||
|
|
||||||
def test_str(self) -> None:
|
def test_str(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
library = pylast.Library(user=self.username, network=self.network)
|
library = pylast.Library(user=self.username, network=self.network)
|
||||||
|
|
||||||
|
@ -28,23 +26,23 @@ class TestPyLastLibrary(TestPyLastWithLastFm):
|
||||||
string = str(library)
|
string = str(library)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert string.endswith("'s Library")
|
self.assert_endswith(string, "'s Library")
|
||||||
|
|
||||||
def test_library_is_hashable(self) -> None:
|
def test_library_is_hashable(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
library = pylast.Library(user=self.username, network=self.network)
|
library = pylast.Library(user=self.username, network=self.network)
|
||||||
|
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
self.helper_is_thing_hashable(library)
|
self.helper_is_thing_hashable(library)
|
||||||
|
|
||||||
def test_cacheable_library(self) -> None:
|
def test_cacheable_library(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
library = pylast.Library(self.username, self.network)
|
library = pylast.Library(self.username, self.network)
|
||||||
|
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
self.helper_validate_cacheable(library, "get_artists")
|
self.helper_validate_cacheable(library, "get_artists")
|
||||||
|
|
||||||
def test_get_user(self) -> None:
|
def test_get_user(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
library = pylast.Library(user=self.username, network=self.network)
|
library = pylast.Library(user=self.username, network=self.network)
|
||||||
user_to_get = self.network.get_user(self.username)
|
user_to_get = self.network.get_user(self.username)
|
||||||
|
|
|
@ -2,20 +2,18 @@
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from flaky import flaky
|
from flaky import flaky
|
||||||
|
|
||||||
import pylast
|
import pylast
|
||||||
|
|
||||||
from .test_pylast import load_secrets
|
from .test_pylast import PyLastTestCase, load_secrets
|
||||||
|
|
||||||
|
|
||||||
@flaky(max_runs=3, min_passes=1)
|
@flaky(max_runs=3, min_passes=1)
|
||||||
class TestPyLastWithLibreFm:
|
class TestPyLastWithLibreFm(PyLastTestCase):
|
||||||
"""Own class for Libre.fm because we don't need the Last.fm setUp"""
|
"""Own class for Libre.fm because we don't need the Last.fm setUp"""
|
||||||
|
|
||||||
def test_libre_fm(self) -> None:
|
def test_libre_fm(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
secrets = load_secrets()
|
secrets = load_secrets()
|
||||||
username = secrets["username"]
|
username = secrets["username"]
|
||||||
|
@ -29,7 +27,7 @@ class TestPyLastWithLibreFm:
|
||||||
# Assert
|
# Assert
|
||||||
assert name == "Radiohead"
|
assert name == "Radiohead"
|
||||||
|
|
||||||
def test_repr(self) -> None:
|
def test_repr(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
secrets = load_secrets()
|
secrets = load_secrets()
|
||||||
username = secrets["username"]
|
username = secrets["username"]
|
||||||
|
@ -40,4 +38,4 @@ class TestPyLastWithLibreFm:
|
||||||
representation = repr(network)
|
representation = repr(network)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert representation.startswith("pylast.LibreFMNetwork(")
|
self.assert_startswith(representation, "pylast.LibreFMNetwork(")
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
@ -16,7 +14,7 @@ from .test_pylast import WRITE_TEST, TestPyLastWithLastFm
|
||||||
|
|
||||||
class TestPyLastNetwork(TestPyLastWithLastFm):
|
class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
@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")
|
||||||
def test_scrobble(self) -> None:
|
def test_scrobble(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "test artist"
|
artist = "test artist"
|
||||||
title = "test title"
|
title = "test title"
|
||||||
|
@ -34,7 +32,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert str(last_scrobble.track.title).lower() == title
|
assert str(last_scrobble.track.title).lower() == title
|
||||||
|
|
||||||
@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")
|
||||||
def test_update_now_playing(self) -> None:
|
def test_update_now_playing(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "Test Artist"
|
artist = "Test Artist"
|
||||||
title = "test title"
|
title = "test title"
|
||||||
|
@ -58,7 +56,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert len(current_track.info["image"])
|
assert len(current_track.info["image"])
|
||||||
assert re.search(r"^http.+$", current_track.info["image"][pylast.SIZE_LARGE])
|
assert re.search(r"^http.+$", current_track.info["image"][pylast.SIZE_LARGE])
|
||||||
|
|
||||||
def test_enable_rate_limiting(self) -> None:
|
def test_enable_rate_limiting(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
assert not self.network.is_rate_limited()
|
assert not self.network.is_rate_limited()
|
||||||
|
|
||||||
|
@ -75,7 +73,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert self.network.is_rate_limited()
|
assert self.network.is_rate_limited()
|
||||||
assert now - then >= 0.2
|
assert now - then >= 0.2
|
||||||
|
|
||||||
def test_disable_rate_limiting(self) -> None:
|
def test_disable_rate_limiting(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
self.network.enable_rate_limit()
|
self.network.enable_rate_limit()
|
||||||
assert self.network.is_rate_limited()
|
assert self.network.is_rate_limited()
|
||||||
|
@ -90,14 +88,14 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert not self.network.is_rate_limited()
|
assert not self.network.is_rate_limited()
|
||||||
|
|
||||||
def test_lastfm_network_name(self) -> None:
|
def test_lastfm_network_name(self):
|
||||||
# Act
|
# Act
|
||||||
name = str(self.network)
|
name = str(self.network)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert name == "Last.fm Network"
|
assert name == "Last.fm Network"
|
||||||
|
|
||||||
def test_geo_get_top_artists(self) -> None:
|
def test_geo_get_top_artists(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Act
|
# Act
|
||||||
artists = self.network.get_geo_top_artists(country="United Kingdom", limit=1)
|
artists = self.network.get_geo_top_artists(country="United Kingdom", limit=1)
|
||||||
|
@ -107,7 +105,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert isinstance(artists[0], pylast.TopItem)
|
assert isinstance(artists[0], pylast.TopItem)
|
||||||
assert isinstance(artists[0].item, pylast.Artist)
|
assert isinstance(artists[0].item, pylast.Artist)
|
||||||
|
|
||||||
def test_geo_get_top_tracks(self) -> None:
|
def test_geo_get_top_tracks(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Act
|
# Act
|
||||||
tracks = self.network.get_geo_top_tracks(
|
tracks = self.network.get_geo_top_tracks(
|
||||||
|
@ -119,7 +117,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert isinstance(tracks[0], pylast.TopItem)
|
assert isinstance(tracks[0], pylast.TopItem)
|
||||||
assert isinstance(tracks[0].item, pylast.Track)
|
assert isinstance(tracks[0].item, pylast.Track)
|
||||||
|
|
||||||
def test_network_get_top_artists_with_limit(self) -> None:
|
def test_network_get_top_artists_with_limit(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Act
|
# Act
|
||||||
artists = self.network.get_top_artists(limit=1)
|
artists = self.network.get_top_artists(limit=1)
|
||||||
|
@ -127,7 +125,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
|
self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
|
||||||
|
|
||||||
def test_network_get_top_tags_with_limit(self) -> None:
|
def test_network_get_top_tags_with_limit(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Act
|
# Act
|
||||||
tags = self.network.get_top_tags(limit=1)
|
tags = self.network.get_top_tags(limit=1)
|
||||||
|
@ -135,7 +133,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_top_list(tags, pylast.Tag)
|
self.helper_only_one_thing_in_top_list(tags, pylast.Tag)
|
||||||
|
|
||||||
def test_network_get_top_tags_with_no_limit(self) -> None:
|
def test_network_get_top_tags_with_no_limit(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Act
|
# Act
|
||||||
tags = self.network.get_top_tags()
|
tags = self.network.get_top_tags()
|
||||||
|
@ -143,7 +141,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_at_least_one_thing_in_top_list(tags, pylast.Tag)
|
self.helper_at_least_one_thing_in_top_list(tags, pylast.Tag)
|
||||||
|
|
||||||
def test_network_get_top_tracks_with_limit(self) -> None:
|
def test_network_get_top_tracks_with_limit(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Act
|
# Act
|
||||||
tracks = self.network.get_top_tracks(limit=1)
|
tracks = self.network.get_top_tracks(limit=1)
|
||||||
|
@ -151,7 +149,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_top_list(tracks, pylast.Track)
|
self.helper_only_one_thing_in_top_list(tracks, pylast.Track)
|
||||||
|
|
||||||
def test_country_top_tracks(self) -> None:
|
def test_country_top_tracks(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
country = self.network.get_country("Croatia")
|
country = self.network.get_country("Croatia")
|
||||||
|
|
||||||
|
@ -161,7 +159,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
||||||
|
|
||||||
def test_country_network_top_tracks(self) -> None:
|
def test_country_network_top_tracks(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Act
|
# Act
|
||||||
things = self.network.get_geo_top_tracks("Croatia", limit=2)
|
things = self.network.get_geo_top_tracks("Croatia", limit=2)
|
||||||
|
@ -169,7 +167,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
||||||
|
|
||||||
def test_tag_top_tracks(self) -> None:
|
def test_tag_top_tracks(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
tag = self.network.get_tag("blues")
|
tag = self.network.get_tag("blues")
|
||||||
|
|
||||||
|
@ -179,7 +177,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
||||||
|
|
||||||
def test_album_data(self) -> None:
|
def test_album_data(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
thing = self.network.get_album("Test Artist", "Test Album")
|
thing = self.network.get_album("Test Artist", "Test Album")
|
||||||
|
|
||||||
|
@ -199,7 +197,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert playcount > 1
|
assert playcount > 1
|
||||||
assert "https://www.last.fm/music/test%2bartist/test%2balbum" == url
|
assert "https://www.last.fm/music/test%2bartist/test%2balbum" == url
|
||||||
|
|
||||||
def test_track_data(self) -> None:
|
def test_track_data(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
thing = self.network.get_track("Test Artist", "test title")
|
thing = self.network.get_track("Test Artist", "test title")
|
||||||
|
|
||||||
|
@ -220,7 +218,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert playcount > 1
|
assert playcount > 1
|
||||||
assert "https://www.last.fm/fr/music/test%2bartist/_/test%2btitle" == url
|
assert "https://www.last.fm/fr/music/test%2bartist/_/test%2btitle" == url
|
||||||
|
|
||||||
def test_country_top_artists(self) -> None:
|
def test_country_top_artists(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
country = self.network.get_country("Ukraine")
|
country = self.network.get_country("Ukraine")
|
||||||
|
|
||||||
|
@ -230,7 +228,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
|
self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
|
||||||
|
|
||||||
def test_caching(self) -> None:
|
def test_caching(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -245,9 +243,9 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
self.network.disable_caching()
|
self.network.disable_caching()
|
||||||
assert not self.network.is_caching_enabled()
|
assert not self.network.is_caching_enabled()
|
||||||
|
|
||||||
def test_album_mbid(self) -> None:
|
def test_album_mbid(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
mbid = "03c91c40-49a6-44a7-90e7-a700edf97a62"
|
mbid = "a6a265bf-9f81-4055-8224-f7ac0aa6b937"
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
album = self.network.get_album_by_mbid(mbid)
|
album = self.network.get_album_by_mbid(mbid)
|
||||||
|
@ -255,10 +253,10 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert isinstance(album, pylast.Album)
|
assert isinstance(album, pylast.Album)
|
||||||
assert album.title == "Believe"
|
assert album.title.lower() == "test"
|
||||||
assert album_mbid == mbid
|
assert album_mbid == mbid
|
||||||
|
|
||||||
def test_artist_mbid(self) -> None:
|
def test_artist_mbid(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
mbid = "7e84f845-ac16-41fe-9ff8-df12eb32af55"
|
mbid = "7e84f845-ac16-41fe-9ff8-df12eb32af55"
|
||||||
|
|
||||||
|
@ -267,9 +265,9 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert isinstance(artist, pylast.Artist)
|
assert isinstance(artist, pylast.Artist)
|
||||||
assert artist.name in ("MusicBrainz Test Artist", "MusicBrainzz Test Artist")
|
assert artist.name == "MusicBrainz Test Artist"
|
||||||
|
|
||||||
def test_track_mbid(self) -> None:
|
def test_track_mbid(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0"
|
mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0"
|
||||||
|
|
||||||
|
@ -282,7 +280,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert track.title == "first"
|
assert track.title == "first"
|
||||||
assert track_mbid == mbid
|
assert track_mbid == mbid
|
||||||
|
|
||||||
def test_init_with_token(self) -> None:
|
def test_init_with_token(self):
|
||||||
# Arrange/Act
|
# Arrange/Act
|
||||||
msg = None
|
msg = None
|
||||||
try:
|
try:
|
||||||
|
@ -297,19 +295,20 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert msg == "Unauthorized Token - This token has not been issued"
|
assert msg == "Unauthorized Token - This token has not been issued"
|
||||||
|
|
||||||
def test_proxy(self) -> None:
|
def test_proxy(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
proxy = "http://example.com:1234"
|
host = "https://example.com"
|
||||||
|
port = 1234
|
||||||
|
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
self.network.enable_proxy(proxy)
|
self.network.enable_proxy(host, port)
|
||||||
assert self.network.is_proxy_enabled()
|
assert self.network.is_proxy_enabled()
|
||||||
assert self.network.proxy == "http://example.com:1234"
|
assert self.network._get_proxy() == ["https://example.com", 1234]
|
||||||
|
|
||||||
self.network.disable_proxy()
|
self.network.disable_proxy()
|
||||||
assert not self.network.is_proxy_enabled()
|
assert not self.network.is_proxy_enabled()
|
||||||
|
|
||||||
def test_album_search(self) -> None:
|
def test_album_search(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album = "Nevermind"
|
album = "Nevermind"
|
||||||
|
|
||||||
|
@ -321,7 +320,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert isinstance(results, list)
|
assert isinstance(results, list)
|
||||||
assert isinstance(results[0], pylast.Album)
|
assert isinstance(results[0], pylast.Album)
|
||||||
|
|
||||||
def test_album_search_images(self) -> None:
|
def test_album_search_images(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album = "Nevermind"
|
album = "Nevermind"
|
||||||
search = self.network.search_for_album(album)
|
search = self.network.search_for_album(album)
|
||||||
|
@ -333,15 +332,15 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert len(images) == 4
|
assert len(images) == 4
|
||||||
|
|
||||||
assert images[pylast.SIZE_SMALL].startswith("https://")
|
self.assert_startswith(images[pylast.SIZE_SMALL], "https://")
|
||||||
assert images[pylast.SIZE_SMALL].endswith(".png")
|
self.assert_endswith(images[pylast.SIZE_SMALL], ".png")
|
||||||
assert "/34s/" in images[pylast.SIZE_SMALL]
|
assert "/34s/" in images[pylast.SIZE_SMALL]
|
||||||
|
|
||||||
assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://")
|
self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://")
|
||||||
assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png")
|
self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png")
|
||||||
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE]
|
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE]
|
||||||
|
|
||||||
def test_artist_search(self) -> None:
|
def test_artist_search(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "Nirvana"
|
artist = "Nirvana"
|
||||||
|
|
||||||
|
@ -353,7 +352,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert isinstance(results, list)
|
assert isinstance(results, list)
|
||||||
assert isinstance(results[0], pylast.Artist)
|
assert isinstance(results[0], pylast.Artist)
|
||||||
|
|
||||||
def test_artist_search_images(self) -> None:
|
def test_artist_search_images(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "Nirvana"
|
artist = "Nirvana"
|
||||||
search = self.network.search_for_artist(artist)
|
search = self.network.search_for_artist(artist)
|
||||||
|
@ -365,15 +364,15 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert len(images) == 5
|
assert len(images) == 5
|
||||||
|
|
||||||
assert images[pylast.SIZE_SMALL].startswith("https://")
|
self.assert_startswith(images[pylast.SIZE_SMALL], "https://")
|
||||||
assert images[pylast.SIZE_SMALL].endswith(".png")
|
self.assert_endswith(images[pylast.SIZE_SMALL], ".png")
|
||||||
assert "/34s/" in images[pylast.SIZE_SMALL]
|
assert "/34s/" in images[pylast.SIZE_SMALL]
|
||||||
|
|
||||||
assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://")
|
self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://")
|
||||||
assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png")
|
self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png")
|
||||||
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE]
|
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE]
|
||||||
|
|
||||||
def test_track_search(self) -> None:
|
def test_track_search(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "Nirvana"
|
artist = "Nirvana"
|
||||||
track = "Smells Like Teen Spirit"
|
track = "Smells Like Teen Spirit"
|
||||||
|
@ -386,7 +385,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
assert isinstance(results, list)
|
assert isinstance(results, list)
|
||||||
assert isinstance(results[0], pylast.Track)
|
assert isinstance(results[0], pylast.Track)
|
||||||
|
|
||||||
def test_track_search_images(self) -> None:
|
def test_track_search_images(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "Nirvana"
|
artist = "Nirvana"
|
||||||
track = "Smells Like Teen Spirit"
|
track = "Smells Like Teen Spirit"
|
||||||
|
@ -399,15 +398,15 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert len(images) == 4
|
assert len(images) == 4
|
||||||
|
|
||||||
assert images[pylast.SIZE_SMALL].startswith("https://")
|
self.assert_startswith(images[pylast.SIZE_SMALL], "https://")
|
||||||
assert images[pylast.SIZE_SMALL].endswith(".png")
|
self.assert_endswith(images[pylast.SIZE_SMALL], ".png")
|
||||||
assert "/34s/" in images[pylast.SIZE_SMALL]
|
assert "/34s/" in images[pylast.SIZE_SMALL]
|
||||||
|
|
||||||
assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://")
|
self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://")
|
||||||
assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png")
|
self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png")
|
||||||
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE]
|
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE]
|
||||||
|
|
||||||
def test_search_get_total_result_count(self) -> None:
|
def test_search_get_total_result_count(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "Nirvana"
|
artist = "Nirvana"
|
||||||
track = "Smells Like Teen Spirit"
|
track = "Smells Like Teen Spirit"
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -12,7 +11,7 @@ from flaky import flaky
|
||||||
|
|
||||||
import pylast
|
import pylast
|
||||||
|
|
||||||
WRITE_TEST = False
|
WRITE_TEST = sys.version_info[:2] == (3, 9)
|
||||||
|
|
||||||
|
|
||||||
def load_secrets(): # pragma: no cover
|
def load_secrets(): # pragma: no cover
|
||||||
|
@ -34,21 +33,29 @@ def load_secrets(): # pragma: no cover
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
def _no_xfail_rerun_filter(err, name, test, plugin) -> bool:
|
class PyLastTestCase:
|
||||||
|
def assert_startswith(self, s, prefix, start=None, end=None):
|
||||||
|
assert s.startswith(prefix, start, end)
|
||||||
|
|
||||||
|
def assert_endswith(self, s, suffix, start=None, end=None):
|
||||||
|
assert s.endswith(suffix, start, end)
|
||||||
|
|
||||||
|
|
||||||
|
def _no_xfail_rerun_filter(err, name, test, plugin):
|
||||||
for _ in test.iter_markers(name="xfail"):
|
for _ in test.iter_markers(name="xfail"):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@flaky(max_runs=3, min_passes=1, rerun_filter=_no_xfail_rerun_filter)
|
@flaky(max_runs=3, min_passes=1, rerun_filter=_no_xfail_rerun_filter)
|
||||||
class TestPyLastWithLastFm:
|
class TestPyLastWithLastFm(PyLastTestCase):
|
||||||
|
|
||||||
secrets = None
|
secrets = None
|
||||||
|
|
||||||
@staticmethod
|
def unix_timestamp(self):
|
||||||
def unix_timestamp() -> int:
|
|
||||||
return int(time.time())
|
return int(time.time())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls) -> None:
|
def setup_class(cls):
|
||||||
if cls.secrets is None:
|
if cls.secrets is None:
|
||||||
cls.secrets = load_secrets()
|
cls.secrets = load_secrets()
|
||||||
|
|
||||||
|
@ -65,8 +72,7 @@ class TestPyLastWithLastFm:
|
||||||
password_hash=password_hash,
|
password_hash=password_hash,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def helper_is_thing_hashable(self, thing):
|
||||||
def helper_is_thing_hashable(thing) -> None:
|
|
||||||
# Arrange
|
# Arrange
|
||||||
things = set()
|
things = set()
|
||||||
|
|
||||||
|
@ -77,8 +83,7 @@ class TestPyLastWithLastFm:
|
||||||
assert thing is not None
|
assert thing is not None
|
||||||
assert len(things) == 1
|
assert len(things) == 1
|
||||||
|
|
||||||
@staticmethod
|
def helper_validate_results(self, a, b, c):
|
||||||
def helper_validate_results(a, b, c) -> None:
|
|
||||||
# Assert
|
# Assert
|
||||||
assert a is not None
|
assert a is not None
|
||||||
assert b is not None
|
assert b is not None
|
||||||
|
@ -89,7 +94,7 @@ class TestPyLastWithLastFm:
|
||||||
assert a == b
|
assert a == b
|
||||||
assert b == c
|
assert b == c
|
||||||
|
|
||||||
def helper_validate_cacheable(self, thing, function_name) -> None:
|
def helper_validate_cacheable(self, thing, function_name):
|
||||||
# Arrange
|
# Arrange
|
||||||
# get thing.function_name()
|
# get thing.function_name()
|
||||||
func = getattr(thing, function_name, None)
|
func = getattr(thing, function_name, None)
|
||||||
|
@ -102,31 +107,27 @@ class TestPyLastWithLastFm:
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_validate_results(result1, result2, result3)
|
self.helper_validate_results(result1, result2, result3)
|
||||||
|
|
||||||
@staticmethod
|
def helper_at_least_one_thing_in_top_list(self, things, expected_type):
|
||||||
def helper_at_least_one_thing_in_top_list(things, expected_type) -> None:
|
|
||||||
# Assert
|
# Assert
|
||||||
assert len(things) > 1
|
assert len(things) > 1
|
||||||
assert isinstance(things, list)
|
assert isinstance(things, list)
|
||||||
assert isinstance(things[0], pylast.TopItem)
|
assert isinstance(things[0], pylast.TopItem)
|
||||||
assert isinstance(things[0].item, expected_type)
|
assert isinstance(things[0].item, expected_type)
|
||||||
|
|
||||||
@staticmethod
|
def helper_only_one_thing_in_top_list(self, things, expected_type):
|
||||||
def helper_only_one_thing_in_top_list(things, expected_type) -> None:
|
|
||||||
# Assert
|
# Assert
|
||||||
assert len(things) == 1
|
assert len(things) == 1
|
||||||
assert isinstance(things, list)
|
assert isinstance(things, list)
|
||||||
assert isinstance(things[0], pylast.TopItem)
|
assert isinstance(things[0], pylast.TopItem)
|
||||||
assert isinstance(things[0].item, expected_type)
|
assert isinstance(things[0].item, expected_type)
|
||||||
|
|
||||||
@staticmethod
|
def helper_only_one_thing_in_list(self, things, expected_type):
|
||||||
def helper_only_one_thing_in_list(things, expected_type) -> None:
|
|
||||||
# Assert
|
# Assert
|
||||||
assert len(things) == 1
|
assert len(things) == 1
|
||||||
assert isinstance(things, list)
|
assert isinstance(things, list)
|
||||||
assert isinstance(things[0], expected_type)
|
assert isinstance(things[0], expected_type)
|
||||||
|
|
||||||
@staticmethod
|
def helper_two_different_things_in_top_list(self, things, expected_type):
|
||||||
def helper_two_different_things_in_top_list(things, expected_type) -> None:
|
|
||||||
# Assert
|
# Assert
|
||||||
assert len(things) == 2
|
assert len(things) == 2
|
||||||
thing1 = things[0]
|
thing1 = things[0]
|
||||||
|
|
|
@ -2,22 +2,20 @@
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import pylast
|
import pylast
|
||||||
|
|
||||||
from .test_pylast import TestPyLastWithLastFm
|
from .test_pylast import TestPyLastWithLastFm
|
||||||
|
|
||||||
|
|
||||||
class TestPyLastTag(TestPyLastWithLastFm):
|
class TestPyLastTag(TestPyLastWithLastFm):
|
||||||
def test_tag_is_hashable(self) -> None:
|
def test_tag_is_hashable(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
tag = self.network.get_top_tags(limit=1)[0]
|
tag = self.network.get_top_tags(limit=1)[0]
|
||||||
|
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
self.helper_is_thing_hashable(tag)
|
self.helper_is_thing_hashable(tag)
|
||||||
|
|
||||||
def test_tag_top_artists(self) -> None:
|
def test_tag_top_artists(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
tag = self.network.get_tag("blues")
|
tag = self.network.get_tag("blues")
|
||||||
|
|
||||||
|
@ -27,7 +25,7 @@ class TestPyLastTag(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
|
self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
|
||||||
|
|
||||||
def test_tag_top_albums(self) -> None:
|
def test_tag_top_albums(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
tag = self.network.get_tag("blues")
|
tag = self.network.get_tag("blues")
|
||||||
|
|
||||||
|
@ -37,7 +35,7 @@ class TestPyLastTag(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_top_list(albums, pylast.Album)
|
self.helper_only_one_thing_in_top_list(albums, pylast.Album)
|
||||||
|
|
||||||
def test_tags(self) -> None:
|
def test_tags(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
tag1 = self.network.get_tag("blues")
|
tag1 = self.network.get_tag("blues")
|
||||||
tag2 = self.network.get_tag("rock")
|
tag2 = self.network.get_tag("rock")
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -15,7 +13,7 @@ from .test_pylast import WRITE_TEST, TestPyLastWithLastFm
|
||||||
|
|
||||||
class TestPyLastTrack(TestPyLastWithLastFm):
|
class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
@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")
|
||||||
def test_love(self) -> None:
|
def test_love(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "Test Artist"
|
artist = "Test Artist"
|
||||||
title = "test title"
|
title = "test title"
|
||||||
|
@ -31,7 +29,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
assert str(loved[0].track.title).lower() == "test title"
|
assert str(loved[0].track.title).lower() == "test title"
|
||||||
|
|
||||||
@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")
|
||||||
def test_unlove(self) -> None:
|
def test_unlove(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = pylast.Artist("Test Artist", self.network)
|
artist = pylast.Artist("Test Artist", self.network)
|
||||||
title = "test title"
|
title = "test title"
|
||||||
|
@ -49,7 +47,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
assert str(loved[0].track.artist) != "Test Artist"
|
assert str(loved[0].track.artist) != "Test Artist"
|
||||||
assert str(loved[0].track.title) != "test title"
|
assert str(loved[0].track.title) != "test title"
|
||||||
|
|
||||||
def test_user_play_count_in_track_info(self) -> None:
|
def test_user_play_count_in_track_info(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "Test Artist"
|
artist = "Test Artist"
|
||||||
title = "test title"
|
title = "test title"
|
||||||
|
@ -63,7 +61,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert count >= 0
|
assert count >= 0
|
||||||
|
|
||||||
def test_user_loved_in_track_info(self) -> None:
|
def test_user_loved_in_track_info(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "Test Artist"
|
artist = "Test Artist"
|
||||||
title = "test title"
|
title = "test title"
|
||||||
|
@ -79,7 +77,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
assert isinstance(loved, bool)
|
assert isinstance(loved, bool)
|
||||||
assert not isinstance(loved, str)
|
assert not isinstance(loved, str)
|
||||||
|
|
||||||
def test_track_is_hashable(self) -> None:
|
def test_track_is_hashable(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = self.network.get_artist("Test Artist")
|
artist = self.network.get_artist("Test Artist")
|
||||||
track = artist.get_top_tracks(stream=False)[0].item
|
track = artist.get_top_tracks(stream=False)[0].item
|
||||||
|
@ -88,7 +86,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
self.helper_is_thing_hashable(track)
|
self.helper_is_thing_hashable(track)
|
||||||
|
|
||||||
def test_track_wiki_content(self) -> None:
|
def test_track_wiki_content(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("Test Artist", "test title", self.network)
|
track = pylast.Track("Test Artist", "test title", self.network)
|
||||||
|
|
||||||
|
@ -99,7 +97,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
assert wiki is not None
|
assert wiki is not None
|
||||||
assert len(wiki) >= 1
|
assert len(wiki) >= 1
|
||||||
|
|
||||||
def test_track_wiki_summary(self) -> None:
|
def test_track_wiki_summary(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("Test Artist", "test title", self.network)
|
track = pylast.Track("Test Artist", "test title", self.network)
|
||||||
|
|
||||||
|
@ -110,17 +108,37 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
assert wiki is not None
|
assert wiki is not None
|
||||||
assert len(wiki) >= 1
|
assert len(wiki) >= 1
|
||||||
|
|
||||||
def test_track_get_duration(self) -> None:
|
def test_track_get_duration(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("Daft Punk", "Something About Us", self.network)
|
track = pylast.Track("Nirvana", "Lithium", self.network)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
duration = track.get_duration()
|
duration = track.get_duration()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert duration >= 100000
|
assert duration >= 200000
|
||||||
|
|
||||||
def test_track_get_album(self) -> None:
|
def test_track_is_streamable(self):
|
||||||
|
# Arrange
|
||||||
|
track = pylast.Track("Nirvana", "Lithium", self.network)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
streamable = track.is_streamable()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert not streamable
|
||||||
|
|
||||||
|
def test_track_is_fulltrack_available(self):
|
||||||
|
# Arrange
|
||||||
|
track = pylast.Track("Nirvana", "Lithium", self.network)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
fulltrack_available = track.is_fulltrack_available()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert not fulltrack_available
|
||||||
|
|
||||||
|
def test_track_get_album(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("Nirvana", "Lithium", self.network)
|
track = pylast.Track("Nirvana", "Lithium", self.network)
|
||||||
|
|
||||||
|
@ -130,7 +148,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert str(album) == "Nirvana - Nevermind"
|
assert str(album) == "Nirvana - Nevermind"
|
||||||
|
|
||||||
def test_track_get_similar(self) -> None:
|
def test_track_get_similar(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("Cher", "Believe", self.network)
|
track = pylast.Track("Cher", "Believe", self.network)
|
||||||
|
|
||||||
|
@ -138,10 +156,14 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
similar = track.get_similar()
|
similar = track.get_similar()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
found = any(str(track.item) == "Cher - Strong Enough" for track in similar)
|
found = False
|
||||||
|
for track in similar:
|
||||||
|
if str(track.item) == "Madonna - Vogue":
|
||||||
|
found = True
|
||||||
|
break
|
||||||
assert found
|
assert found
|
||||||
|
|
||||||
def test_track_get_similar_limits(self) -> None:
|
def test_track_get_similar_limits(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("Cher", "Believe", self.network)
|
track = pylast.Track("Cher", "Believe", self.network)
|
||||||
|
|
||||||
|
@ -151,7 +173,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
assert len(track.get_similar(limit=None)) >= 23
|
assert len(track.get_similar(limit=None)) >= 23
|
||||||
assert len(track.get_similar(limit=0)) >= 23
|
assert len(track.get_similar(limit=0)) >= 23
|
||||||
|
|
||||||
def test_tracks_notequal(self) -> None:
|
def test_tracks_notequal(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track1 = pylast.Track("Test Artist", "test title", self.network)
|
track1 = pylast.Track("Test Artist", "test title", self.network)
|
||||||
track2 = pylast.Track("Test Artist", "Test Track", self.network)
|
track2 = pylast.Track("Test Artist", "Test Track", self.network)
|
||||||
|
@ -160,7 +182,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert track1 != track2
|
assert track1 != track2
|
||||||
|
|
||||||
def test_track_title_prop_caps(self) -> None:
|
def test_track_title_prop_caps(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("test artist", "test title", self.network)
|
track = pylast.Track("test artist", "test title", self.network)
|
||||||
|
|
||||||
|
@ -170,7 +192,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert title == "Test Title"
|
assert title == "Test Title"
|
||||||
|
|
||||||
def test_track_listener_count(self) -> None:
|
def test_track_listener_count(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("test artist", "test title", self.network)
|
track = pylast.Track("test artist", "test title", self.network)
|
||||||
|
|
||||||
|
@ -180,7 +202,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert count > 21
|
assert count > 21
|
||||||
|
|
||||||
def test_album_tracks(self) -> None:
|
def test_album_tracks(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
album = pylast.Album("Test Artist", "Test", self.network)
|
album = pylast.Album("Test Artist", "Test", self.network)
|
||||||
|
|
||||||
|
@ -194,7 +216,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
assert len(tracks) == 1
|
assert len(tracks) == 1
|
||||||
assert url.startswith("https://www.last.fm/music/test")
|
assert url.startswith("https://www.last.fm/music/test")
|
||||||
|
|
||||||
def test_track_eq_none_is_false(self) -> None:
|
def test_track_eq_none_is_false(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track1 = None
|
track1 = None
|
||||||
track2 = pylast.Track("Test Artist", "test title", self.network)
|
track2 = pylast.Track("Test Artist", "test title", self.network)
|
||||||
|
@ -202,7 +224,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
assert track1 != track2
|
assert track1 != track2
|
||||||
|
|
||||||
def test_track_ne_none_is_true(self) -> None:
|
def test_track_ne_none_is_true(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track1 = None
|
track1 = None
|
||||||
track2 = pylast.Track("Test Artist", "test title", self.network)
|
track2 = pylast.Track("Test Artist", "test title", self.network)
|
||||||
|
@ -210,7 +232,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
# Act / Assert
|
# Act / Assert
|
||||||
assert track1 != track2
|
assert track1 != track2
|
||||||
|
|
||||||
def test_track_get_correction(self) -> None:
|
def test_track_get_correction(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("Guns N' Roses", "mrbrownstone", self.network)
|
track = pylast.Track("Guns N' Roses", "mrbrownstone", self.network)
|
||||||
|
|
||||||
|
@ -220,7 +242,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert corrected_track_name == "Mr. Brownstone"
|
assert corrected_track_name == "Mr. Brownstone"
|
||||||
|
|
||||||
def test_track_with_no_mbid(self) -> None:
|
def test_track_with_no_mbid(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
track = pylast.Track("Static-X", "Set It Off", self.network)
|
track = pylast.Track("Static-X", "Set It Off", self.network)
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
"""
|
"""
|
||||||
Integration (not unit) tests for pylast.py
|
Integration (not unit) tests for pylast.py
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -18,7 +16,7 @@ from .test_pylast import TestPyLastWithLastFm
|
||||||
|
|
||||||
|
|
||||||
class TestPyLastUser(TestPyLastWithLastFm):
|
class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
def test_repr(self) -> None:
|
def test_repr(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -26,9 +24,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
representation = repr(user)
|
representation = repr(user)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert representation.startswith("pylast.User('RJ',")
|
self.assert_startswith(representation, "pylast.User('RJ',")
|
||||||
|
|
||||||
def test_str(self) -> None:
|
def test_str(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -38,7 +36,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert string == "RJ"
|
assert string == "RJ"
|
||||||
|
|
||||||
def test_equality(self) -> None:
|
def test_equality(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user_1a = self.network.get_user("RJ")
|
user_1a = self.network.get_user("RJ")
|
||||||
user_1b = self.network.get_user("RJ")
|
user_1b = self.network.get_user("RJ")
|
||||||
|
@ -50,7 +48,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert user_1a != user_2
|
assert user_1a != user_2
|
||||||
assert user_1a != not_a_user
|
assert user_1a != not_a_user
|
||||||
|
|
||||||
def test_get_name(self) -> None:
|
def test_get_name(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -60,7 +58,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert name == "RJ"
|
assert name == "RJ"
|
||||||
|
|
||||||
def test_get_user_registration(self) -> None:
|
def test_get_user_registration(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -76,7 +74,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Just check date because of timezones
|
# Just check date because of timezones
|
||||||
assert "2002-11-20 " in registered
|
assert "2002-11-20 " in registered
|
||||||
|
|
||||||
def test_get_user_unixtime_registration(self) -> None:
|
def test_get_user_unixtime_registration(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -87,7 +85,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Just check date because of timezones
|
# Just check date because of timezones
|
||||||
assert unixtime_registered == 1037793040
|
assert unixtime_registered == 1037793040
|
||||||
|
|
||||||
def test_get_countryless_user(self) -> None:
|
def test_get_countryless_user(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Currently test_user has no country set:
|
# Currently test_user has no country set:
|
||||||
lastfm_user = self.network.get_user("test_user")
|
lastfm_user = self.network.get_user("test_user")
|
||||||
|
@ -98,7 +96,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert country is None
|
assert country is None
|
||||||
|
|
||||||
def test_user_get_country(self) -> None:
|
def test_user_get_country(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user("RJ")
|
lastfm_user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -108,7 +106,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert str(country) == "United Kingdom"
|
assert str(country) == "United Kingdom"
|
||||||
|
|
||||||
def test_user_equals_none(self) -> None:
|
def test_user_equals_none(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user(self.username)
|
lastfm_user = self.network.get_user(self.username)
|
||||||
|
|
||||||
|
@ -118,7 +116,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert not value
|
assert not value
|
||||||
|
|
||||||
def test_user_not_equal_to_none(self) -> None:
|
def test_user_not_equal_to_none(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user(self.username)
|
lastfm_user = self.network.get_user(self.username)
|
||||||
|
|
||||||
|
@ -128,7 +126,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert value
|
assert value
|
||||||
|
|
||||||
def test_now_playing_user_with_no_scrobbles(self) -> None:
|
def test_now_playing_user_with_no_scrobbles(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Currently test-account has no scrobbles:
|
# Currently test-account has no scrobbles:
|
||||||
user = self.network.get_user("test-account")
|
user = self.network.get_user("test-account")
|
||||||
|
@ -139,7 +137,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert current_track is None
|
assert current_track is None
|
||||||
|
|
||||||
def test_love_limits(self) -> None:
|
def test_love_limits(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
# Currently test-account has at least 23 loved tracks:
|
# Currently test-account has at least 23 loved tracks:
|
||||||
user = self.network.get_user("test-user")
|
user = self.network.get_user("test-user")
|
||||||
|
@ -150,7 +148,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert len(user.get_loved_tracks(limit=None)) >= 23
|
assert len(user.get_loved_tracks(limit=None)) >= 23
|
||||||
assert len(user.get_loved_tracks(limit=0)) >= 23
|
assert len(user.get_loved_tracks(limit=0)) >= 23
|
||||||
|
|
||||||
def test_user_is_hashable(self) -> None:
|
def test_user_is_hashable(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user(self.username)
|
user = self.network.get_user(self.username)
|
||||||
|
|
||||||
|
@ -171,7 +169,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# # Assert
|
# # Assert
|
||||||
# self.assertGreaterEqual(len(tracks), 0)
|
# self.assertGreaterEqual(len(tracks), 0)
|
||||||
|
|
||||||
def test_pickle(self) -> None:
|
def test_pickle(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
|
@ -189,7 +187,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert lastfm_user == loaded_user
|
assert lastfm_user == loaded_user
|
||||||
|
|
||||||
@pytest.mark.xfail
|
@pytest.mark.xfail
|
||||||
def test_cacheable_user(self) -> None:
|
def test_cacheable_user(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_authenticated_user()
|
lastfm_user = self.network.get_authenticated_user()
|
||||||
|
|
||||||
|
@ -203,7 +201,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
lastfm_user, "get_recent_tracks"
|
lastfm_user, "get_recent_tracks"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_user_get_top_tags_with_limit(self) -> None:
|
def test_user_get_top_tags_with_limit(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -213,7 +211,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_top_list(tags, pylast.Tag)
|
self.helper_only_one_thing_in_top_list(tags, pylast.Tag)
|
||||||
|
|
||||||
def test_user_top_tracks(self) -> None:
|
def test_user_top_tracks(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user("RJ")
|
lastfm_user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -223,14 +221,14 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
self.helper_two_different_things_in_top_list(things, pylast.Track)
|
||||||
|
|
||||||
def helper_assert_chart(self, chart, expected_type) -> None:
|
def helper_assert_chart(self, chart, expected_type):
|
||||||
# Assert
|
# Assert
|
||||||
assert chart is not None
|
assert chart is not None
|
||||||
assert len(chart) > 0
|
assert len(chart) > 0
|
||||||
assert isinstance(chart[0], pylast.TopItem)
|
assert isinstance(chart[0], pylast.TopItem)
|
||||||
assert isinstance(chart[0].item, expected_type)
|
assert isinstance(chart[0].item, expected_type)
|
||||||
|
|
||||||
def helper_get_assert_charts(self, thing, date) -> None:
|
def helper_get_assert_charts(self, thing, date):
|
||||||
# Arrange
|
# Arrange
|
||||||
album_chart, track_chart = None, None
|
album_chart, track_chart = None, None
|
||||||
(from_date, to_date) = date
|
(from_date, to_date) = date
|
||||||
|
@ -247,14 +245,14 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
self.helper_assert_chart(album_chart, pylast.Album)
|
self.helper_assert_chart(album_chart, pylast.Album)
|
||||||
self.helper_assert_chart(track_chart, pylast.Track)
|
self.helper_assert_chart(track_chart, pylast.Track)
|
||||||
|
|
||||||
def helper_dates_valid(self, dates) -> None:
|
def helper_dates_valid(self, dates):
|
||||||
# Assert
|
# Assert
|
||||||
assert len(dates) >= 1
|
assert len(dates) >= 1
|
||||||
assert isinstance(dates[0], tuple)
|
assert isinstance(dates[0], tuple)
|
||||||
(start, end) = dates[0]
|
(start, end) = dates[0]
|
||||||
assert start < end
|
assert start < end
|
||||||
|
|
||||||
def test_user_charts(self) -> None:
|
def test_user_charts(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user("RJ")
|
lastfm_user = self.network.get_user("RJ")
|
||||||
dates = lastfm_user.get_weekly_chart_dates()
|
dates = lastfm_user.get_weekly_chart_dates()
|
||||||
|
@ -263,7 +261,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
self.helper_get_assert_charts(lastfm_user, dates[0])
|
self.helper_get_assert_charts(lastfm_user, dates[0])
|
||||||
|
|
||||||
def test_user_top_artists(self) -> None:
|
def test_user_top_artists(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user(self.username)
|
lastfm_user = self.network.get_user(self.username)
|
||||||
|
|
||||||
|
@ -273,7 +271,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
|
self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
|
||||||
|
|
||||||
def test_user_top_albums(self) -> None:
|
def test_user_top_albums(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -287,7 +285,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert len(top_album.info["image"])
|
assert len(top_album.info["image"])
|
||||||
assert re.search(r"^http.+$", top_album.info["image"][pylast.SIZE_LARGE])
|
assert re.search(r"^http.+$", top_album.info["image"][pylast.SIZE_LARGE])
|
||||||
|
|
||||||
def test_user_tagged_artists(self) -> None:
|
def test_user_tagged_artists(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user(self.username)
|
lastfm_user = self.network.get_user(self.username)
|
||||||
tags = ["artisttagola"]
|
tags = ["artisttagola"]
|
||||||
|
@ -300,7 +298,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_list(artists, pylast.Artist)
|
self.helper_only_one_thing_in_list(artists, pylast.Artist)
|
||||||
|
|
||||||
def test_user_tagged_albums(self) -> None:
|
def test_user_tagged_albums(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user(self.username)
|
lastfm_user = self.network.get_user(self.username)
|
||||||
tags = ["albumtagola"]
|
tags = ["albumtagola"]
|
||||||
|
@ -313,7 +311,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_list(albums, pylast.Album)
|
self.helper_only_one_thing_in_list(albums, pylast.Album)
|
||||||
|
|
||||||
def test_user_tagged_tracks(self) -> None:
|
def test_user_tagged_tracks(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user(self.username)
|
lastfm_user = self.network.get_user(self.username)
|
||||||
tags = ["tracktagola"]
|
tags = ["tracktagola"]
|
||||||
|
@ -326,7 +324,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
self.helper_only_one_thing_in_list(tracks, pylast.Track)
|
self.helper_only_one_thing_in_list(tracks, pylast.Track)
|
||||||
|
|
||||||
def test_user_subscriber(self) -> None:
|
def test_user_subscriber(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
subscriber = self.network.get_user("RJ")
|
subscriber = self.network.get_user("RJ")
|
||||||
non_subscriber = self.network.get_user("Test User")
|
non_subscriber = self.network.get_user("Test User")
|
||||||
|
@ -339,7 +337,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert subscriber_is_subscriber
|
assert subscriber_is_subscriber
|
||||||
assert not non_subscriber_is_subscriber
|
assert not non_subscriber_is_subscriber
|
||||||
|
|
||||||
def test_user_get_image(self) -> None:
|
def test_user_get_image(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -347,9 +345,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
url = user.get_image()
|
url = user.get_image()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert url.startswith("https://")
|
self.assert_startswith(url, "https://")
|
||||||
|
|
||||||
def test_user_get_library(self) -> None:
|
def test_user_get_library(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user(self.username)
|
user = self.network.get_user(self.username)
|
||||||
|
|
||||||
|
@ -359,7 +357,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert isinstance(library, pylast.Library)
|
assert isinstance(library, pylast.Library)
|
||||||
|
|
||||||
def test_get_recent_tracks_from_to(self) -> None:
|
def test_get_recent_tracks_from_to(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user("RJ")
|
lastfm_user = self.network.get_user("RJ")
|
||||||
start = dt.datetime(2011, 7, 21, 15, 10)
|
start = dt.datetime(2011, 7, 21, 15, 10)
|
||||||
|
@ -376,7 +374,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert str(tracks[0].track.artist) == "Johnny Cash"
|
assert str(tracks[0].track.artist) == "Johnny Cash"
|
||||||
assert str(tracks[0].track.title) == "Ring of Fire"
|
assert str(tracks[0].track.title) == "Ring of Fire"
|
||||||
|
|
||||||
def test_get_recent_tracks_limit_none(self) -> None:
|
def test_get_recent_tracks_limit_none(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user("bbc6music")
|
lastfm_user = self.network.get_user("bbc6music")
|
||||||
start = dt.datetime(2020, 2, 15, 15, 00)
|
start = dt.datetime(2020, 2, 15, 15, 00)
|
||||||
|
@ -395,7 +393,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert str(tracks[0].track.artist) == "Seun Kuti & Egypt 80"
|
assert str(tracks[0].track.artist) == "Seun Kuti & Egypt 80"
|
||||||
assert str(tracks[0].track.title) == "Struggles Sounds"
|
assert str(tracks[0].track.title) == "Struggles Sounds"
|
||||||
|
|
||||||
def test_get_recent_tracks_is_streamable(self) -> None:
|
def test_get_recent_tracks_is_streamable(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
lastfm_user = self.network.get_user("bbc6music")
|
lastfm_user = self.network.get_user("bbc6music")
|
||||||
start = dt.datetime(2020, 2, 15, 15, 00)
|
start = dt.datetime(2020, 2, 15, 15, 00)
|
||||||
|
@ -412,7 +410,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert inspect.isgenerator(tracks)
|
assert inspect.isgenerator(tracks)
|
||||||
|
|
||||||
def test_get_playcount(self) -> None:
|
def test_get_playcount(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -422,7 +420,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert playcount >= 128387
|
assert playcount >= 128387
|
||||||
|
|
||||||
def test_get_image(self) -> None:
|
def test_get_image(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -430,10 +428,10 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
image = user.get_image()
|
image = user.get_image()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert image.startswith("https://")
|
self.assert_startswith(image, "https://")
|
||||||
assert image.endswith(".png")
|
self.assert_endswith(image, ".png")
|
||||||
|
|
||||||
def test_get_url(self) -> None:
|
def test_get_url(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("RJ")
|
user = self.network.get_user("RJ")
|
||||||
|
|
||||||
|
@ -443,7 +441,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
# Assert
|
# Assert
|
||||||
assert url == "https://www.last.fm/user/rj"
|
assert url == "https://www.last.fm/user/rj"
|
||||||
|
|
||||||
def test_get_weekly_artist_charts(self) -> None:
|
def test_get_weekly_artist_charts(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("bbc6music")
|
user = self.network.get_user("bbc6music")
|
||||||
|
|
||||||
|
@ -455,7 +453,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert artist is not None
|
assert artist is not None
|
||||||
assert isinstance(artist.network, pylast.LastFMNetwork)
|
assert isinstance(artist.network, pylast.LastFMNetwork)
|
||||||
|
|
||||||
def test_get_weekly_track_charts(self) -> None:
|
def test_get_weekly_track_charts(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
user = self.network.get_user("bbc6music")
|
user = self.network.get_user("bbc6music")
|
||||||
|
|
||||||
|
@ -467,7 +465,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert track is not None
|
assert track is not None
|
||||||
assert isinstance(track.network, pylast.LastFMNetwork)
|
assert isinstance(track.network, pylast.LastFMNetwork)
|
||||||
|
|
||||||
def test_user_get_track_scrobbles(self) -> None:
|
def test_user_get_track_scrobbles(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "France Gall"
|
artist = "France Gall"
|
||||||
title = "Laisse Tomber Les Filles"
|
title = "Laisse Tomber Les Filles"
|
||||||
|
@ -481,7 +479,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
|
||||||
assert str(scrobbles[0].track.artist) == "France Gall"
|
assert str(scrobbles[0].track.artist) == "France Gall"
|
||||||
assert scrobbles[0].track.title == "Laisse Tomber Les Filles"
|
assert scrobbles[0].track.title == "Laisse Tomber Les Filles"
|
||||||
|
|
||||||
def test_cacheable_user_get_track_scrobbles(self) -> None:
|
def test_cacheable_user_get_track_scrobbles(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
artist = "France Gall"
|
artist = "France Gall"
|
||||||
title = "Laisse Tomber Les Filles"
|
title = "Laisse Tomber Les Filles"
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -20,51 +18,12 @@ def mock_network():
|
||||||
"fdasfdsafsaf not unicode",
|
"fdasfdsafsaf not unicode",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_get_cache_key(artist) -> None:
|
def test_get_cache_key(artist):
|
||||||
request = pylast._Request(mock_network(), "some_method", params={"artist": artist})
|
request = pylast._Request(mock_network(), "some_method", params={"artist": artist})
|
||||||
request._get_cache_key()
|
request._get_cache_key()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("obj", [pylast.Artist("B\xe9l", mock_network())])
|
@pytest.mark.parametrize("obj", [pylast.Artist("B\xe9l", mock_network())])
|
||||||
def test_cast_and_hash(obj) -> None:
|
def test_cast_and_hash(obj):
|
||||||
assert isinstance(str(obj), str)
|
assert type(str(obj)) is str
|
||||||
assert isinstance(hash(obj), int)
|
assert isinstance(hash(obj), int)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"test_input, expected",
|
|
||||||
[
|
|
||||||
(
|
|
||||||
# Plain text
|
|
||||||
'<album mbid="">test album name</album>',
|
|
||||||
'<album mbid="">test album name</album>',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# Contains Unicode ENQ Enquiry control character
|
|
||||||
'<album mbid="">test album \u0005name</album>',
|
|
||||||
'<album mbid="">test album name</album>',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test__remove_invalid_xml_chars(test_input: str, expected: str) -> None:
|
|
||||||
assert pylast._remove_invalid_xml_chars(test_input) == expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"test_input, expected",
|
|
||||||
[
|
|
||||||
(
|
|
||||||
# Plain text
|
|
||||||
'<album mbid="">test album name</album>',
|
|
||||||
'<?xml version="1.0" ?><album mbid="">test album name</album>',
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# Contains Unicode ENQ Enquiry control character
|
|
||||||
'<album mbid="">test album \u0005name</album>',
|
|
||||||
'<?xml version="1.0" ?><album mbid="">test album name</album>',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test__parse_response(test_input: str, expected: str) -> None:
|
|
||||||
doc = pylast._parse_response(test_input)
|
|
||||||
assert doc.toxml() == expected
|
|
||||||
|
|
41
tox.ini
41
tox.ini
|
@ -1,40 +1,29 @@
|
||||||
[tox]
|
[tox]
|
||||||
requires =
|
envlist =
|
||||||
tox>=4.2
|
py{py3, 310, 39, 38, 37, 36}
|
||||||
env_list =
|
|
||||||
lint
|
|
||||||
py{py3, 313, 312, 311, 310, 39, 38}
|
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
extras =
|
passenv =
|
||||||
tests
|
|
||||||
pass_env =
|
|
||||||
FORCE_COLOR
|
|
||||||
PYLAST_API_KEY
|
PYLAST_API_KEY
|
||||||
PYLAST_API_SECRET
|
PYLAST_API_SECRET
|
||||||
PYLAST_PASSWORD_HASH
|
PYLAST_PASSWORD_HASH
|
||||||
PYLAST_USERNAME
|
PYLAST_USERNAME
|
||||||
|
extras =
|
||||||
|
tests
|
||||||
commands =
|
commands =
|
||||||
{envpython} -m pytest -v -s -W all \
|
pytest -v -s -W all --cov pylast --cov tests --cov-report term-missing --random-order {posargs}
|
||||||
--cov pylast \
|
|
||||||
--cov tests \
|
|
||||||
--cov-report html \
|
|
||||||
--cov-report term-missing \
|
|
||||||
--cov-report xml \
|
|
||||||
--random-order \
|
|
||||||
{posargs}
|
|
||||||
|
|
||||||
[testenv:lint]
|
|
||||||
skip_install = true
|
|
||||||
deps =
|
|
||||||
pre-commit
|
|
||||||
pass_env =
|
|
||||||
PRE_COMMIT_COLOR
|
|
||||||
commands =
|
|
||||||
pre-commit run --all-files --show-diff-on-failure
|
|
||||||
|
|
||||||
[testenv:venv]
|
[testenv:venv]
|
||||||
deps =
|
deps =
|
||||||
ipdb
|
ipdb
|
||||||
commands =
|
commands =
|
||||||
{posargs}
|
{posargs}
|
||||||
|
|
||||||
|
[testenv:lint]
|
||||||
|
passenv =
|
||||||
|
PRE_COMMIT_COLOR
|
||||||
|
skip_install = true
|
||||||
|
deps =
|
||||||
|
pre-commit
|
||||||
|
commands =
|
||||||
|
pre-commit run --all-files --show-diff-on-failure
|
||||||
|
|
Loading…
Reference in a new issue