Compare commits

...

232 commits

Author SHA1 Message Date
Hirad 88bb8ea789 Update README.md
Some checks failed
Release drafter / update_release_draft (push) Has been cancelled
Deploy / Build & verify package (push) Has been cancelled
Deploy / Publish in-dev package to test.pypi.org (push) Has been cancelled
Deploy / Publish released package to pypi.org (push) Has been cancelled
Lint / lint (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
Test / test (ubuntu-latest, 3.8) (push) Has been cancelled
Test / test (ubuntu-latest, 3.9) (push) Has been cancelled
Test / test (ubuntu-latest, pypy3.10) (push) Has been cancelled
Test / Test successful (push) Has been cancelled
2024-07-07 09:16:54 +03:30
Hirad 6ae051157f Update README.md
Some checks are pending
Deploy / Build & verify package (push) Waiting to run
Deploy / Publish in-dev package to test.pypi.org (push) Blocked by required conditions
Deploy / Publish released package to pypi.org (push) Blocked by required conditions
Lint / lint (push) Waiting to run
Release drafter / update_release_draft (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.8) (push) Waiting to run
Test / test (ubuntu-latest, 3.9) (push) Waiting to run
Test / test (ubuntu-latest, pypy3.10) (push) Waiting to run
Test / Test successful (push) Blocked by required conditions
2024-07-07 09:16:25 +03:30
Hirad c260d7b83f change libre.fm to music.lonestar.it
Some checks are pending
Deploy / Build & verify package (push) Waiting to run
Deploy / Publish in-dev package to test.pypi.org (push) Blocked by required conditions
Deploy / Publish released package to pypi.org (push) Blocked by required conditions
Lint / lint (push) Waiting to run
Release drafter / update_release_draft (push) Waiting to run
Test / test (ubuntu-latest, 3.10) (push) Waiting to run
Test / test (ubuntu-latest, 3.11) (push) Waiting to run
Test / test (ubuntu-latest, 3.12) (push) Waiting to run
Test / test (ubuntu-latest, 3.13) (push) Waiting to run
Test / test (ubuntu-latest, 3.8) (push) Waiting to run
Test / test (ubuntu-latest, 3.9) (push) Waiting to run
Test / test (ubuntu-latest, pypy3.10) (push) Waiting to run
Test / Test successful (push) Blocked by required conditions
2024-07-07 09:03:18 +03:30
Hugo van Kemenade 25904371de
[pre-commit.ci] pre-commit autoupdate (#458) 2024-07-02 05:34:35 -06:00
Hugo van Kemenade 184d0328a9 Configure pyproject-fmt to add Python 3.13 classifier 2024-07-02 14:29:20 +03:00
pre-commit-ci[bot] aceaa69c9a
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.4 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.4...v0.5.0)
- [github.com/asottile/blacken-docs: 1.16.0 → 1.18.0](https://github.com/asottile/blacken-docs/compare/1.16.0...1.18.0)
- [github.com/python-jsonschema/check-jsonschema: 0.28.4 → 0.28.6](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.4...0.28.6)
- [github.com/rhysd/actionlint: v1.7.0 → v1.7.1](https://github.com/rhysd/actionlint/compare/v1.7.0...v1.7.1)
- [github.com/tox-dev/pyproject-fmt: 1.7.0 → 2.1.3](https://github.com/tox-dev/pyproject-fmt/compare/1.7.0...2.1.3)
2024-07-01 17:24:06 +00:00
Hugo van Kemenade de737fb4ee
Update config (#457) 2024-05-22 22:50:30 +03:00
Hugo van Kemenade 6f97f93dcc Update config 2024-05-22 22:46:47 +03:00
Hugo van Kemenade a23e1c5181
Fix expected result in test and refactor (#456) 2024-05-22 22:38:49 +03:00
Hugo van Kemenade 35b264bae4 Refactor 2024-05-22 22:29:05 +03:00
Hugo van Kemenade 353e32bd6b Fix expected result in test 2024-05-22 22:26:42 +03:00
Hugo van Kemenade 0fa96932a4
Update example_test_pylast.yaml Link in README.md (#455) 2024-05-22 22:08:13 +03:00
pre-commit-ci[bot] 90c3614d6a [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-05-22 05:10:35 +00:00
Christian McKinnon fa68aa4ae8 Update example_test_pylast.yaml Link in README.md 2024-05-22 11:48:48 +07:00
Hugo van Kemenade 8a26cf88d8
[pre-commit.ci] pre-commit autoupdate (#453) 2024-04-01 20:41:01 +03:00
pre-commit-ci[bot] 82cb504871
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.3.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.3.4)
- [github.com/psf/black-pre-commit-mirror: 24.1.1 → 24.3.0](https://github.com/psf/black-pre-commit-mirror/compare/24.1.1...24.3.0)
2024-04-01 17:16:48 +00:00
Hugo van Kemenade 9d4283a924
Update github-actions (#450) 2024-03-01 08:06:49 +02:00
Hugo van Kemenade e4d2ebc4a0
Update test.yml 2024-03-01 08:02:36 +02:00
renovate[bot] d5f1c3d3ac
Update github-actions 2024-03-01 01:13:50 +00:00
Hugo van Kemenade e737ae2f34
Add support for Python 3.13 (#448) 2024-02-05 21:35:25 +02:00
Hugo van Kemenade f4547a5821 Add Prettier to pre-commit 2024-02-05 21:27:22 +02:00
Hugo van Kemenade a14a50a333 Scrutinizer was removed in 2019 2024-02-05 21:27:22 +02:00
Hugo van Kemenade 77d1b0009c Add support for Python 3.13 2024-02-05 21:27:22 +02:00
Hugo van Kemenade d505d57fc4
Replace Flake8 with Ruff (#447) 2024-02-05 20:53:51 +02:00
Hugo van Kemenade 3890cb4c04 Add {envpython} and --cov-report html, multiline for clarity 2024-02-04 22:09:24 +02:00
Hugo van Kemenade 5bccda1102 Update config 2024-02-04 22:06:20 +02:00
Hugo van Kemenade 36d89a69e8 Replace Flake8 with Ruff 2024-02-04 22:06:20 +02:00
Hugo van Kemenade a28dea1158
Update github-actions (#446) 2024-02-04 21:10:29 +02:00
Hugo van Kemenade 6c888343c8
Pin codecov/codecov-action to v3.1.5 2024-02-04 12:00:33 -07:00
renovate[bot] ffebde28e2
Update github-actions 2024-02-04 18:59:28 +00:00
Hugo van Kemenade befb5aeceb
Double read timeout to fix 'The read operation timed out' (#445) 2024-02-04 20:58:44 +02:00
Hugo van Kemenade 370ff77f21 Double read timeout to fix 'The read operation timed out'
5 seconds is the default
2024-01-22 09:23:41 +02:00
Hugo van Kemenade cdfc23b5e4
Update github-actions (#443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-15 19:24:32 +02:00
pre-commit-ci[bot] d5fe263c23
[pre-commit.ci] pre-commit autoupdate (#444)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2024-01-01 10:31:29 -07:00
renovate[bot] e90d717b66
Update github-actions 2024-01-01 01:18:15 +00:00
Eugene Simonov b4c8dc7282
Add type annotations to methods that take timestamp parameter (#442)
Co-authored-by: Eugene Simonov <eugene.simonov@evergen.com.au>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2023-12-29 12:00:10 -07:00
Hugo van Kemenade 6f30559a3a
Fix incorrect docstrings (#439)
Changes proposed in this pull request:
- fix docstrings with inaccurate descriptions

While going through the pyLast documentation, I noticed that a few
functions had inaccurate descriptions. The proper use of these functions
should be intuitive from their names, but I thought it still might be
useful to fix their docstrings. Let me know if there are any issues with
these changes. Thanks!
2023-10-27 07:00:57 +03:00
Mia Bilka a91bac007d Fixed incorrect docstrings 2023-10-26 18:02:01 -07:00
pre-commit-ci[bot] c0f9f4222a
[pre-commit.ci] pre-commit autoupdate (#437)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-10-02 14:16:48 -06:00
renovate[bot] 47872dbb32
Update actions/checkout action to v4 (#436)
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://togithub.com/actions/checkout) | action |
major | `v3` -> `v4` |

---

### Release Notes

<details>
<summary>actions/checkout (actions/checkout)</summary>

###
[`v4`](https://togithub.com/actions/checkout/blob/HEAD/CHANGELOG.md#v400)

[Compare Source](https://togithub.com/actions/checkout/compare/v3...v4)

- [Support fetching without the --progress
option](https://togithub.com/actions/checkout/pull/1067)
-   [Update to node20](https://togithub.com/actions/checkout/pull/1436)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "on the first day of the month" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://developer.mend.io/github/pylast/pylast).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4wLjMiLCJ1cGRhdGVkSW5WZXIiOiIzNy4wLjMiLCJ0YXJnZXRCcmFuY2giOiJtYWluIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-01 08:37:46 +03:00
pre-commit-ci[bot] 74392c4d71
[pre-commit.ci] pre-commit autoupdate (#434)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2023-08-22 14:22:24 +03:00
Hugo van Kemenade 97eab1719f
Remove default 'cache-dependency-path: pyproject.toml' (#435) 2023-07-24 17:36:32 +03:00
Hugo van Kemenade e4b7af41f9 Remove default 'cache-dependency-path: pyproject.toml'
Committed via https://github.com/asottile/all-repos
2023-07-24 17:33:42 +03:00
Hugo van Kemenade 7f1c90cfea
CI: Replace pypy3.9 with pypy3.10 (#433) 2023-06-18 14:45:26 +03:00
Hugo van Kemenade 68c0197028 CI: Replace pypy3.9 with pypy3.10
Committed via https://github.com/asottile/all-repos
2023-06-18 14:42:29 +03:00
Hugo van Kemenade 9e62e37b1e
Update mheap/github-action-required-labels action to v5 (#432) 2023-06-08 21:24:55 +03:00
Hugo van Kemenade c26c5f86aa Update on the first day of the month 2023-06-08 21:22:05 +03:00
renovate[bot] f7a73aa62f
Update mheap/github-action-required-labels action to v5 2023-06-08 17:29:12 +00:00
Hugo van Kemenade 34e0e54fea
Merge pull request #431 from pylast/deploy 2023-06-06 19:24:16 +03:00
Hugo van Kemenade 02da99f4b0 Use hynek/build-and-inspect-python-package 2023-06-06 19:20:56 +03:00
Hugo van Kemenade 5f302e0813
Merge pull request #430 from pylast/rm-3.7 2023-06-06 19:15:22 +03:00
Hugo van Kemenade 47eda3ea70 Refactor: make helper methods static 2023-06-03 18:10:01 +03:00
Hugo van Kemenade 7da76f49bd Refactor: replace redundant helper methods, no need with pytest 2023-06-03 18:03:05 +03:00
Hugo van Kemenade 1c669d8bb0 Fix test: now returns a png 2023-06-03 17:55:00 +03:00
Hugo van Kemenade 0f59831dc2 Drop support for EOL Python 3.7 2023-06-03 17:41:35 +03:00
Hugo van Kemenade 8d8263ce42
Merge pull request #428 from pylast/deploy 2023-04-18 06:31:29 -06:00
Hugo van Kemenade 07ce433fc0
Merge pull request #427 from pylast/add-3.12 2023-04-18 06:21:36 -06:00
Hugo van Kemenade dc4bd8474c Test newest PyPy 2023-04-18 06:19:54 -06:00
Hugo van Kemenade b05b8454f5 Update pre-commit 2023-04-18 06:09:25 -06:00
Hugo van Kemenade 56fc297371 Publish to PyPI with a Trusted Publisher 2023-04-18 06:08:52 -06:00
Hugo van Kemenade 9f59dd770c Add support for Python 3.12 2023-04-18 06:04:33 -06:00
Hugo van Kemenade f0ea480334
Merge pull request #426 from pylast/pre-commit-ci-update-config 2023-04-18 05:49:37 -06:00
Hugo van Kemenade 165e4761f4
Merge pull request #424 from pylast/all-repos_autofix_fix-deprecated-repository_url
Replace deprecated repository_url with repository-url
2023-04-18 05:34:12 -06:00
Hugo van Kemenade cdb88b9bbb Update pre-commit 2023-04-18 05:28:30 -06:00
pre-commit-ci[bot] 879591e1cc [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-04-03 19:10:56 +00:00
pre-commit-ci[bot] 6a7a23cd9a
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.12.0 → 23.3.0](https://github.com/psf/black/compare/22.12.0...23.3.0)
- [github.com/asottile/blacken-docs: v1.12.1 → 1.13.0](https://github.com/asottile/blacken-docs/compare/v1.12.1...1.13.0)
- [github.com/pre-commit/pygrep-hooks: v1.9.0 → v1.10.0](https://github.com/pre-commit/pygrep-hooks/compare/v1.9.0...v1.10.0)
- [github.com/tox-dev/pyproject-fmt: 0.4.1 → 0.9.2](https://github.com/tox-dev/pyproject-fmt/compare/0.4.1...0.9.2)
- [github.com/abravalheri/validate-pyproject: v0.10.1 → v0.12.2](https://github.com/abravalheri/validate-pyproject/compare/v0.10.1...v0.12.2)
- [github.com/tox-dev/tox-ini-fmt: 0.5.2 → 1.0.0](https://github.com/tox-dev/tox-ini-fmt/compare/0.5.2...1.0.0)
2023-04-03 19:10:46 +00:00
Hugo van Kemenade 793ae1453c
Merge pull request #425 from pylast/renovate/github-actions
chore(deps): update mheap/github-action-required-labels action to v4
2023-04-02 14:03:18 +03:00
renovate[bot] 111334328e
chore(deps): update mheap/github-action-required-labels action to v4 2023-04-02 09:06:18 +00:00
Hugo van Kemenade 15f0ccfd58 Replace deprecated repository_url with repository-url
Committed via https://github.com/asottile/all-repos
2023-03-19 15:53:12 +02:00
Hugo van Kemenade 94432d62b0
Merge pull request #423 from pylast/all-repos_autofix_all-repos-sed 2023-01-29 13:29:31 +02:00
Hugo van Kemenade dab0a5b661 Bump isort to fix Poetry
Re: https://github.com/PyCQA/isort/pull/2078

Committed via https://github.com/asottile/all-repos
2023-01-29 13:27:16 +02:00
Hugo van Kemenade 7f07babdf4
Merge pull request #421 from ndm13/patch-1
Document how to authenticate with a session key
2023-01-06 09:54:32 +02:00
Hugo van Kemenade f5ea06c6c9
Include "import pylast" in both blocks 2023-01-06 09:49:52 +02:00
Hugo van Kemenade 4ea1df0930
Merge pull request #420 from pylast/pre-commit-ci-update-config 2023-01-05 18:46:19 +02:00
Hugo van Kemenade e63ecc7bea Autolabel pre-commit PRs with 'changelog: skip' 2023-01-05 18:42:43 +02:00
Hugo van Kemenade 28403386a8 Bump Black 2023-01-05 18:41:16 +02:00
Hugo van Kemenade 8647cbdd48 Make alternative clearer via own code blocks 2023-01-05 18:36:38 +02:00
ndm13 a37ac22e6c
Add code from pylast #407 to readme
Describe authentication with OAuth token
2023-01-02 15:09:16 -05:00
pre-commit-ci[bot] 7861fd55bd
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.1.0 → v3.3.1](https://github.com/asottile/pyupgrade/compare/v3.1.0...v3.3.1)
- [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0)
- [github.com/PyCQA/isort: 5.10.1 → 5.11.4](https://github.com/PyCQA/isort/compare/5.10.1...5.11.4)
- [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0)
- [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0)
- [github.com/tox-dev/pyproject-fmt: 0.3.5 → 0.4.1](https://github.com/tox-dev/pyproject-fmt/compare/0.3.5...0.4.1)
2023-01-02 18:09:58 +00:00
Hugo van Kemenade 219be9f61a
Merge pull request #419 from pylast/renovate/github-actions 2022-12-31 00:07:48 +02:00
renovate[bot] 8169fad09d
chore(deps): update mheap/github-action-required-labels action to v3 2022-12-30 21:56:31 +00:00
Hugo van Kemenade 0152d98b28
Merge pull request #418 from pylast/all-repos_autofix_add-3.12-dev 2022-11-09 14:00:31 +02:00
Hugo van Kemenade d03e25fc6c Test Python 3.12-dev
Committed via https://github.com/asottile/all-repos
2022-11-09 13:44:07 +02:00
Hugo van Kemenade ce76c03581
Merge pull request #416 from pylast/test-3.11-final
Test on Python 3.11 final
2022-10-25 18:58:49 +03:00
Hugo van Kemenade 7f3518fc1a
Test on Python 3.11 final 2022-10-25 18:32:50 +03:00
Hugo van Kemenade 0560f711c3
Merge pull request #414 from pylast/pre-commit-ci-update-config 2022-10-25 14:53:29 +03:00
pre-commit-ci[bot] 41e0dd604e [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.34.0 → v2.38.2](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.38.2)
- [github.com/psf/black: 22.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0)
- [github.com/PyCQA/flake8: 4.0.1 → 5.0.4](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.4)
- [github.com/asottile/setup-cfg-fmt: v1.20.1 → v2.0.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.1...v2.0.0)
2022-10-25 14:50:00 +03:00
Hugo van Kemenade 8ea5b42d92
Merge pull request #413 from pylast/migrate-packaging 2022-10-25 14:42:39 +03:00
Hugo van Kemenade dbbbcfec44 pyLast 5.1+ supports Python 3.7-3.11 2022-09-26 14:20:35 +03:00
Hugo van Kemenade fc288040a8 Migrate from setuptools + setuptools_scm to hatchling + hatch-vcs 2022-09-26 14:01:16 +03:00
Hugo van Kemenade 98943d606e Migrate from setup.* to pyproject.toml 2022-09-26 11:45:08 +03:00
Hugo van Kemenade 54a9f04f8f
Merge pull request #412 from pylast/fix-test_track_get_duration 2022-09-26 11:35:02 +03:00
Hugo van Kemenade 7f1de76f6e Fix test_track_get_duration 2022-09-26 11:30:50 +03:00
Hugo van Kemenade ece37c4659
Merge pull request #410 from pylast/all-repos_autofix_pypa/gh-action-pypi-publish 2022-07-25 21:44:24 +03:00
Hugo van Kemenade 8a967b52f4 Replace deprecated pypa/gh-action-pypi-publish@master with @release/v1
Committed via https://github.com/asottile/all-repos
2022-07-25 21:21:03 +03:00
Hugo van Kemenade d35eb5220f
Merge pull request #408 from pylast/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-07-04 22:10:41 +03:00
pre-commit-ci[bot] aeba21dedb
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.32.0 → v2.34.0](https://github.com/asottile/pyupgrade/compare/v2.32.0...v2.34.0)
- [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0)
- [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0)
2022-07-04 17:40:19 +00:00
Hugo van Kemenade af71e116e0
Merge pull request #406 from pylast/renovate/github-actions 2022-06-21 20:05:13 +03:00
renovate[bot] 7df369dfff
chore(deps): update mheap/github-action-required-labels action to v2 2022-06-21 16:53:11 +00:00
Hugo van Kemenade 790351928f
Merge pull request #405 from pylast/renovate/github-actions
chore(deps): update actions/setup-python action to v4
2022-06-08 20:01:47 +03:00
Hugo van Kemenade 1e9d7d8c94
A Python version is required for v4 2022-06-08 19:55:48 +03:00
Renovate Bot 139e77707d
chore(deps): update actions/setup-python action to v4 2022-06-08 16:08:22 +00:00
Hugo van Kemenade 8ed1ff2a3e
Merge pull request #404 from pylast/renovate/github-actions
chore(deps): update pre-commit/action action to v3
2022-06-06 07:53:46 +03:00
Renovate Bot 3823d77a35
chore(deps): update pre-commit/action action to v3 2022-06-05 20:27:34 +00:00
Hugo van Kemenade 9f6fcf34fb
Merge pull request #401 from pylast/renovate/github-actions 2022-05-02 18:26:56 +03:00
Renovate Bot 75e2dd5f2e
chore(deps): update github-actions to v3 2022-05-02 15:20:10 +00:00
Hugo van Kemenade 11f70bfee9
Merge pull request #400 from pylast/renovate/configure 2022-05-02 18:19:51 +03:00
Hugo van Kemenade 4fc4a6ad89 Allow combining major bumps for GHA 2022-05-02 18:13:09 +03:00
Hugo van Kemenade 861182253c Move to .github and add labels 2022-05-02 18:13:09 +03:00
Renovate Bot ea421db602 chore(deps): add renovate.json 2022-05-02 18:13:09 +03:00
Hugo van Kemenade d3ba0be1a3
Merge pull request #399 from pylast/add-3.11
Support Python 3.11
2022-05-02 18:13:01 +03:00
Hugo van Kemenade afbafe1e76 Fix test 2022-05-02 15:06:07 +03:00
Hugo van Kemenade dec407d958 Add final 'Test successful' to simplify PR status check requirements 2022-05-02 14:59:08 +03:00
Hugo van Kemenade fa94ed0263 Support Python 3.11 2022-05-02 14:59:08 +03:00
Hugo van Kemenade 5b0c879fa0 Update config 2022-05-02 14:59:08 +03:00
Hugo van Kemenade aefa7cef1b
Merge pull request #395 from pylast/cleanup 2022-04-06 18:05:20 +03:00
Hugo van Kemenade caf0915062
Merge pull request #396 from pylast/pre-commit-ci-update-config 2022-04-04 20:47:36 +03:00
Hugo van Kemenade 2478980ca5 For some reason the earlier track is returning duration=0 2022-04-04 20:44:32 +03:00
pre-commit-ci[bot] c1a8a9455f
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.31.0 → v2.31.1](https://github.com/asottile/pyupgrade/compare/v2.31.0...v2.31.1)
- [github.com/psf/black: 21.12b0 → 22.3.0](https://github.com/psf/black/compare/21.12b0...22.3.0)
- [github.com/asottile/blacken-docs: v1.12.0 → v1.12.1](https://github.com/asottile/blacken-docs/compare/v1.12.0...v1.12.1)
- [github.com/asottile/setup-cfg-fmt: v1.20.0 → v1.20.1](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.0...v1.20.1)
2022-04-04 17:19:23 +00:00
Hugo van Kemenade 4f37ba41bd
Initialise float as 0.0
And skip Iterator type for now to avoid its complex subscripting
2022-04-03 18:11:42 +03:00
Hugo van Kemenade ac991cbd2c Types and typos 2022-04-03 12:58:44 +03:00
Hugo van Kemenade 14e091c870 autotyping: --annotate-imprecise-magics: add imprecise type annotations for some additional magic methods 2022-04-03 12:49:01 +03:00
Hugo van Kemenade 7b9c73acb7 autotyping: --annotate-magics: add type annotation to certain magic methods 2022-04-03 12:47:58 +03:00
Hugo van Kemenade 54ea354a7a autotyping: --int-param, --float-param, --str-param, --bytes-param: add an annotation to any parameter for which the default is a literal int, float, str, or bytes object 2022-04-03 12:47:10 +03:00
Hugo van Kemenade 5ab3e53a44 autotyping: --bool-param: add a : bool annotation to any function parameter with a default of True or False 2022-04-03 12:46:14 +03:00
Hugo van Kemenade eb4af40d64 autotyping: --scalar-return: add a return annotation to functions that only return literal bool, str, bytes, int, or float objects 2022-04-03 12:45:23 +03:00
Hugo van Kemenade 6c3f3afb3a autotyping: --none-return: add a -> None return type to functions without any return, yield, or raise in their body 2022-04-03 12:45:02 +03:00
Hugo van Kemenade 4e5fe31572 Rename variable e to element 2022-04-03 12:38:16 +03:00
Hugo van Kemenade b0f2f5fe13 For some reason the earlier track is returning duration=0 2022-04-03 12:35:28 +03:00
Hugo van Kemenade 95c8b16564 Upgrade Black to fix Click 2022-04-03 12:35:28 +03:00
Hugo van Kemenade 549437b640 Fix 'a a...' to 'an a...' 2022-04-03 12:33:38 +03:00
Hugo van Kemenade b373de6c68 More f-strings 2022-04-03 12:33:38 +03:00
Hugo van Kemenade 5f8d150652 Remove redundant _get_cache_backend and add some typing 2022-04-03 12:33:38 +03:00
Hugo van Kemenade 83aeaddc43
Merge pull request #394 from pylast/update-logging 2022-04-03 10:57:22 +03:00
Hugo van Kemenade dd8836e59b Logging: log method names at INFO level, also log API return data at DEBUG level 2022-03-03 13:15:26 +02:00
Hugo van Kemenade 5c9509dfc4
Merge pull request #392 from pylast/all-repos_autofix_all-repos-sed 2022-03-01 11:51:54 +02:00
Hugo van Kemenade b726227d5d Upgrade to actions/setup-python@v3
Committed via https://github.com/asottile/all-repos
2022-03-01 11:48:22 +02:00
Hugo van Kemenade f28a74791d
Merge pull request #390 from pylast/fix-album-mbid-none 2022-02-27 20:18:30 +02:00
Hugo van Kemenade fe7484b3ca If album has no MBID, album.get_getmbid() returns None 2022-02-27 16:46:29 +02:00
Hugo van Kemenade 00f92eb436
Merge pull request #391 from pylast/fix-coverage 2022-02-27 16:45:33 +02:00
Hugo van Kemenade f7090f26a0 Output coverage XML for Codecov to upload 2022-02-27 16:38:08 +02:00
Hugo van Kemenade 4ae6c16f57
Merge pull request #379 from pylast/httpx 2022-02-27 16:22:07 +02:00
Hugo van Kemenade 1a45c3b919 Allow setting multiple proxies + some cleanup 2022-02-27 16:18:41 +02:00
Hugo van Kemenade da2e7152ba Update blacken-docs to match main black 2022-02-27 16:18:41 +02:00
Hugo van Kemenade a418f64b15 Simplify _unicode 2022-02-27 16:18:41 +02:00
Hugo van Kemenade 122c870312 Replace _string with str 2022-02-27 16:18:41 +02:00
Hugo van Kemenade 44ade40579 Replace http.client with HTTPX 2022-02-27 16:18:41 +02:00
Hugo van Kemenade 26db2bc68b
Merge pull request #388 from pylast/rm-deprecations 2022-02-27 16:17:06 +02:00
Hugo van Kemenade bb05699252 Remove deprecated is_streamable and is_fulltrack_available 2022-02-27 16:13:04 +02:00
Hugo van Kemenade 7f4bea6f07
Merge pull request #387 from pylast/revert-383-add-3.6 2022-02-27 16:11:20 +02:00
Hugo van Kemenade d610721167 Drop support for Python EOL 3.6 2022-02-27 16:08:33 +02:00
Hugo van Kemenade 6465f4cf51
Update link to deploy action 2022-01-31 12:50:28 +02:00
Hugo van Kemenade bafc3fe673
Merge pull request #385 from pylast/rm-mergify 2022-01-24 22:57:48 +02:00
Hugo van Kemenade b151dd0c93 Remove Mergify, use native GitHub auto-merge instead 2022-01-24 22:54:54 +02:00
Hugo van Kemenade dd869b5183
Merge pull request #384 from pylast/deprecate-streamable 2022-01-24 22:37:16 +02:00
Hugo van Kemenade 3ffe7cf65a test_get_userplaycount now passes 2022-01-24 22:26:16 +02:00
Hugo van Kemenade 1841fb66dc This test now passes, although some other MBID searches are still broken 2022-01-24 22:07:03 +02:00
Hugo van Kemenade d672e89f23 Is an xfail passing unexpectedly? Make it fail 2022-01-24 21:15:11 +02:00
Hugo van Kemenade 3b7cb9c8c7 Deprecate is_streamable and is_fulltrack_available 2022-01-24 19:05:06 +02:00
Hugo van Kemenade e14f51a32a
Merge pull request #383 from pylast/add-3.6 2022-01-24 19:04:27 +02:00
Hugo van Kemenade c63e0a75ef Restore support for Python 3.6 2022-01-24 18:02:15 +02:00
Hugo van Kemenade a204055798
Merge pull request #382 from pylast/remove-invalid-xml-chars-from-response 2022-01-12 22:14:45 +02:00
Hugo van Kemenade 9676714dcf Strip invalid XML characters from response 2022-01-12 13:04:34 +02:00
Hugo van Kemenade 2469a6ea47
Merge pull request #378 from pylast/rm-3.6 2022-01-11 15:35:19 +02:00
Hugo van Kemenade d46aabc372
Merge pull request #380 from pylast/pre-commit-ci-update-config 2022-01-03 19:06:47 +02:00
pre-commit-ci[bot] 129e4392fc
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.29.1 → v2.31.0](https://github.com/asottile/pyupgrade/compare/v2.29.1...v2.31.0)
- [github.com/psf/black: 21.11b1 → 21.12b0](https://github.com/psf/black/compare/21.11b1...21.12b0)
- [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0)
- [github.com/tox-dev/tox-ini-fmt: 0.5.1 → 0.5.2](https://github.com/tox-dev/tox-ini-fmt/compare/0.5.1...0.5.2)
2022-01-03 16:58:33 +00:00
Hugo van Kemenade 8b66e69004 Drop support for soon-EOL Python 3.6 2021-11-21 18:43:32 +02:00
Hugo van Kemenade 2966ecfd13
Merge pull request #367 from ChandlerSwift/fix-limit-on-user-top-tracks 2021-11-21 18:30:38 +02:00
Hugo van Kemenade 4d4d167394
Merge pull request #377 from pylast/speedup 2021-11-21 18:21:06 +02:00
Hugo van Kemenade b48fbb4eb8 Speedup: Use faster importlib.metadata for getting version 2021-11-21 18:07:40 +02:00
Hugo van Kemenade d3ee0e4942
Merge pull request #376 from pylast/setup-py-to-cfg 2021-11-21 17:51:06 +02:00
Hugo van Kemenade 25cf4165ea Fix typo 2021-11-21 17:44:55 +02:00
Hugo van Kemenade 754d94374b Use actions/setup-python's pip cache 2021-11-21 17:44:49 +02:00
Hugo van Kemenade b3fb55586c Convert setup.py to static setup.cfg and format with setup-cfg-fmt 2021-11-21 16:42:04 +02:00
Hugo van Kemenade a0bdc3c5ac
Merge pull request #375 from pylast/all-repos_autofix_all-repos-sed 2021-11-10 12:48:44 +02:00
Hugo van Kemenade 3a7c83998f Replace MBID for missing test album with a real one 2021-11-10 11:44:10 +02:00
Hugo van Kemenade ae7d4e3625 Replace deprecated pypy3 with pypy-3.8
Committed via https://github.com/asottile/all-repos
2021-11-10 10:53:05 +02:00
Hugo van Kemenade 7b98775fa0
Merge pull request #374 from pylast/rename-master-to-main 2021-10-19 14:01:07 +03:00
Hugo van Kemenade 05b4ad8c62 Disable the flaky write tests 2021-10-19 13:57:47 +03:00
Hugo van Kemenade c41f831d82 Fix test 2021-10-19 13:17:20 +03:00
Hugo van Kemenade e5b9f2aa19 Add colour to tests 2021-10-19 13:11:21 +03:00
Hugo van Kemenade 9072b98a18 Rename master to main, use 3.10 final, add workflow_dispatch 2021-10-19 13:08:53 +03:00
Hugo van Kemenade 3db88e98ce
Merge pull request #372 from pylast/add-3.10 2021-10-04 20:48:19 +03:00
Hugo van Kemenade 031b3ebbb1 Add support for Python 3.10 2021-10-04 20:36:05 +03:00
Hugo van Kemenade aebbe53a61
Merge pull request #371 from pylast/pre-commit-ci-update-config 2021-10-04 20:12:19 +03:00
pre-commit-ci[bot] 73e3b1b9ed
[pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v2.23.1 → v2.29.0](https://github.com/asottile/pyupgrade/compare/v2.23.1...v2.29.0)
- [github.com/psf/black: 21.7b0 → 21.9b0](https://github.com/psf/black/compare/21.7b0...21.9b0)
- [github.com/asottile/blacken-docs: v1.10.0 → v1.11.0](https://github.com/asottile/blacken-docs/compare/v1.10.0...v1.11.0)
2021-10-04 16:45:32 +00:00
Hugo van Kemenade fd520ad47b
Merge pull request #370 from pylast/pre-commmit 2021-08-02 21:44:01 +03:00
Hugo van Kemenade a850f093f0 track.getInfo with mbid is broken at Last.fm: https://support.last.fm/t/track-getinfo-with-mbid-returns-6-track-not-found/47905 2021-08-02 21:09:28 +03:00
Hugo van Kemenade ddb1b1e501 Fix test 2021-08-02 20:50:21 +03:00
Hugo van Kemenade 72491f7a99 Last.fm now even skips an empty <content/> when no bio 2021-08-02 20:46:56 +03:00
Hugo van Kemenade c8a64dbee9 Test image is now gif 2021-08-02 20:42:20 +03:00
Hugo van Kemenade 20cd3ff475 Update pre-commit and add quarterly autoupdate_schedule 2021-08-02 20:28:45 +03:00
Hugo van Kemenade e193106bde
Merge pull request #369 from tieubinhco/master
Remove artist.shout("<3") in README.md
2021-05-29 10:25:57 +03:00
Tran Tieu Binh 1a35601f51 Remove blank line 2021-05-29 14:24:41 +07:00
Tran Tieu Binh ce2c1e6f76 Remove artist.shout("<3")
There is no shout() method for the artist object.
2021-05-29 13:57:30 +07:00
Hugo van Kemenade a516a44c32 New changes are documented in GH Releases 2021-04-30 22:53:37 +03:00
Hugo van Kemenade 55107d12ba
Merge pull request #358 from kvanzuijlen/feature/fix_for_userloved_userplaycount 2021-04-30 22:19:21 +03:00
Chandler Swift 4e645ca134
Set get_top_tracks limit even if it's None
To get an unlimited number of top tracks, `_get_things` expects
`params['limit']` to be set to `None`. However, this can't happen here
because `None` is falsy.

Fixes #366.
2021-04-27 15:29:26 -05:00
Hugo van Kemenade aad860a222
Add 4.2.0 [CI skip] 2021-03-14 19:37:02 +02:00
Hugo van Kemenade 6fa502ea17 Update release draft title 2021-03-14 18:07:50 +02:00
Hugo van Kemenade 585da81d56
Merge pull request #356 from kvanzuijlen/feature/unsafe_tempfile 2021-03-14 17:55:53 +02:00
Hugo van Kemenade 5e1dc22fea
Merge pull request #363 from pylast/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-02-08 20:12:44 +02:00
pre-commit-ci[bot] e87da1efde
[pre-commit.ci] pre-commit autoupdate 2021-02-08 16:28:18 +00:00
Hugo van Kemenade 20692d1c9e
Merge pull request #362 from pylast/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-02-01 18:33:23 +02:00
pre-commit-ci[bot] c6e983b579
[pre-commit.ci] pre-commit autoupdate 2021-02-01 16:27:39 +00:00
Koen van Zuijlen ea1f2b42f8 Merge branch 'master' into feature/unsafe_tempfile 2021-01-12 10:19:29 +01:00
Koen van Zuijlen 10803a0a63 Merge branch 'master' into feature/fix_for_userloved_userplaycount 2021-01-12 10:19:24 +01:00
Koen van Zuijlen a41f2e0f36 Merge remote-tracking branch 'pylast/master' 2021-01-12 10:18:59 +01:00
Hugo van Kemenade 26ffcf5ad6
Add 4.1.0 2021-01-04 20:29:53 +02:00
Hugo van Kemenade b7700b58c7
Merge pull request #360 from pylast/updates 2021-01-04 20:25:12 +02:00
Hugo van Kemenade d7ee88ebe2
Merge pull request #359 from pylast/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-01-04 18:43:59 +02:00
Hugo van Kemenade 2bf906af17 Update copyright year 2021-01-04 18:42:55 +02:00
Hugo van Kemenade c0fb459458 Update test config 2021-01-04 18:41:16 +02:00
Hugo van Kemenade 6328b9e106 Name lint job after workflow 2021-01-04 18:40:56 +02:00
Hugo van Kemenade 7a235fcc6e Update release config 2021-01-04 18:40:46 +02:00
Hugo van Kemenade f3ee6a71a7 Update label config 2021-01-04 18:40:36 +02:00
pre-commit-ci[bot] 9241a02637
[pre-commit.ci] pre-commit autoupdate 2021-01-04 16:26:38 +00:00
pre-commit-ci[bot] 0c546976b9 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-01-02 00:06:16 +00:00
Koen van Zuijlen 6fe9aa632b Fix for user play count and user loved 2021-01-02 00:48:32 +01:00
Koen van Zuijlen 36b2eeb297 Code improvement 2020-12-30 17:12:32 +01:00
Koen van Zuijlen e9bef6db68 Bugfix for caching between sessions 2020-12-30 17:11:38 +01:00
pre-commit-ci[bot] eca1db8622 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2020-12-30 14:59:18 +00:00
Koen van Zuijlen 2d2e73c1bc Fixed unsafe tempfile and fixed some basic problems 2020-12-30 15:56:35 +01:00
Koen van Zuijlen 123a00c5e3 Merge remote-tracking branch 'pylast/master' 2020-12-30 14:10:16 +01:00
Hugo van Kemenade 3fcf45062d Blacken docs 2020-12-30 13:09:06 +02:00
Hugo van Kemenade 6c66279957 Blacken docs 2020-12-30 13:07:03 +02:00
Hugo van Kemenade bf7cd60774 Update formatting, versions and links 2020-12-30 13:06:41 +02:00
Hugo van Kemenade 34690f68cc
Merge pull request #336 from kvanzuijlen/master 2020-12-30 12:55:30 +02:00
Koen van Zuijlen c851b82a1d Reverted temporary files change 2020-12-29 22:12:43 +01:00
Koen van Zuijlen b992d26138 Bugfix for creation of temporary files 2020-12-29 21:19:46 +01:00
Koen van Zuijlen 421c80a617 Merge branch 'streaming' 2020-12-29 20:59:29 +01:00
Koen van Zuijlen 7193eb0d1f Merge remote-tracking branch 'pylast/master' 2020-12-24 20:43:06 +01:00
36 changed files with 1171 additions and 877 deletions

View file

@ -11,7 +11,6 @@ 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

View file

@ -12,8 +12,11 @@
Please include **code** that reproduces the issue. Please include **code** that reproduces the issue.
The [best reproductions](https://stackoverflow.com/help/minimal-reproducible-example) are [self-contained scripts](https://ericlippert.com/2014/03/05/how-to-debug-small-programs/) with minimal dependencies. The [best reproductions](https://stackoverflow.com/help/minimal-reproducible-example)
are
[self-contained scripts](https://ericlippert.com/2014/03/05/how-to-debug-small-programs/)
with minimal dependencies.
```python ```python
code goes here # code goes here
``` ```

11
.github/labels.yml vendored
View file

@ -91,18 +91,21 @@
- 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
- color: 0052cc
description: "Documentation"
name: docs
- color: f4660e - color: f4660e
description: "" description: ""
name: Hacktoberfest name: Hacktoberfest
- color: f4660e - color: f4660e
description: "To credit accepted Hacktoberfest PRs" description: "To credit accepted Hacktoberfest PRs"
name: hacktoberfest-accepted name: hacktoberfest-accepted
- color: d65e88
description: "Deploy and release"
name: release
- color: fef2c0 - color: fef2c0
description: "" description: "Unit tests, linting, CI, etc."
name: test name: test

View file

@ -1,4 +1,4 @@
name-template: "Release $RESOLVED_VERSION" name-template: "$RESOLVED_VERSION"
tag-template: "$RESOLVED_VERSION" tag-template: "$RESOLVED_VERSION"
categories: categories:
@ -22,9 +22,12 @@ categories:
exclude-labels: exclude-labels:
- "changelog: skip" - "changelog: skip"
template: | autolabeler:
## Changes - label: "changelog: skip"
branch:
- "/pre-commit-ci-update-config/"
template: |
$CHANGES $CHANGES
version-resolver: version-resolver:

13
.github/renovate.json vendored Normal file
View file

@ -0,0 +1,13 @@
{
"$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"]
}

View file

@ -2,56 +2,74 @@ name: Deploy
on: on:
push: push:
branches: branches: [main]
- master tags: ["*"]
pull_request:
branches: [main]
release: release:
types: types:
- published - published
workflow_dispatch:
permissions:
contents: read
jobs: jobs:
build: # Always build & lint package.
if: github.repository == 'pylast/pylast' build-package:
runs-on: ubuntu-20.04 name: Build & verify package
runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Cache - uses: hynek/build-and-inspect-python-package@v2
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:
path: ~/.cache/pip name: Packages
key: deploy-${{ hashFiles('**/setup.py') }} path: dist
restore-keys: |
deploy-
- name: Set up Python - name: Upload package to Test PyPI
uses: actions/setup-python@v2 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
python-version: 3.9 repository-url: https://test.pypi.org/legacy/
- name: Install dependencies # Upload to real PyPI on GitHub Releases.
run: | release-pypi:
python -m pip install -U pip name: Publish released package to pypi.org
python -m pip install -U setuptools twine wheel if: |
github.repository_owner == 'pylast'
&& github.event.action == 'published'
runs-on: ubuntu-latest
needs: build-package
- name: Build package permissions:
run: | id-token: write
python setup.py --version
python setup.py sdist --format=gztar bdist_wheel
twine check dist/*
- name: Publish package to PyPI steps:
if: github.event.action == 'published' - name: Download packages built by build-and-inspect-python-package
uses: pypa/gh-action-pypi-publish@master uses: actions/download-artifact@v4
with: with:
user: __token__ name: Packages
password: ${{ secrets.pypi_password }} path: dist
- name: Publish package to TestPyPI - name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@master uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.test_pypi_password }}
repository_url: https://test.pypi.org/legacy/

View file

@ -1,15 +1,21 @@
name: Sync labels name: Sync labels
permissions:
pull-requests: write
on: on:
push: push:
branches: branches:
- master - main
paths: paths:
- .github/labels.yml - .github/labels.yml
workflow_dispatch:
jobs: jobs:
build: sync:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: micnncim/action-label-syncer@v1 - uses: micnncim/action-label-syncer@v1
with: with:
prune: false prune: false

View file

@ -1,12 +1,22 @@
name: Lint name: Lint
on: [push, pull_request] on: [push, pull_request, workflow_dispatch]
env:
FORCE_COLOR: 1
PIP_DISABLE_PIP_VERSION_CHECK: 1
permissions:
contents: read
jobs: jobs:
build: lint:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-python@v2 - uses: actions/setup-python@v5
- uses: pre-commit/action@v2.0.0 with:
python-version: "3.x"
cache: pip
- uses: pre-commit/action@v3.0.1

View file

@ -4,14 +4,31 @@ 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:
- master - main
# 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 == 'pylast/pylast' if: github.repository_owner == '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 "master" # Drafts your next release notes as pull requests are merged into "main"
- uses: release-drafter/release-drafter@v5 - uses: release-drafter/release-drafter@v6
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

22
.github/workflows/require-pr-label.yml vendored Normal file
View file

@ -0,0 +1,22 @@
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"

View file

@ -1,44 +1,28 @@
name: Test name: Test
on: [push, pull_request] on: [push, pull_request, workflow_dispatch]
env: env:
FORCE_COLOR: 1 FORCE_COLOR: 1
jobs: jobs:
build: test:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev", "pypy3"] python-version: ["pypy3.10", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-20.04] os: [ubuntu-latest]
include:
# Include new variables for Codecov
- { codecov-flag: GHA_Ubuntu2004, os: ubuntu-20.04 }
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Get pip cache dir cache: pip
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: |
@ -47,7 +31,6 @@ jobs:
python -m pip install -U tox python -m pip install -U tox
- name: Tox tests - name: Tox tests
shell: bash
run: | run: |
tox -e py tox -e py
env: env:
@ -57,7 +40,15 @@ jobs:
PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }} PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }}
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3.1.5
with: with:
flags: ${{ matrix.codecov-flag }} flags: ${{ matrix.os }}
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

View file

@ -1,8 +0,0 @@
pull_request_rules:
- name: Automatic merge on approval
conditions:
- label=automerge
- status-success=build
actions:
merge:
method: merge

View file

@ -1,42 +1,74 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v2.7.4 rev: v0.5.0
hooks: hooks:
- id: pyupgrade - id: ruff
args: ["--py36-plus"] args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black-pre-commit-mirror
rev: 20.8b1 rev: 24.4.2
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/PyCQA/isort - repo: https://github.com/asottile/blacken-docs
rev: 5.6.4 rev: 1.18.0
hooks: hooks:
- id: isort - id: blacken-docs
args: [--target-version=py38]
- repo: https://gitlab.com/pycqa/flake8 additional_dependencies: [black]
rev: 3.8.4
hooks:
- id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.7.0
hooks:
- id: python-check-blanket-noqa
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0 rev: v4.6.0
hooks: hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json
- id: check-toml
- id: check-yaml - 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
rev: 0.28.6
hooks:
- id: check-github-workflows
- id: check-renovate
- repo: https://github.com/rhysd/actionlint
rev: v1.7.1
hooks:
- id: actionlint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 2.1.3
hooks:
- id: pyproject-fmt
- 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: 0.5.0 rev: 1.3.1
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

View file

@ -1,9 +0,0 @@
checks:
python:
code_rating: true
duplicate_code: true
filter:
excluded_paths:
- '*/test/*'
tools:
external_code_coverage: true

View file

@ -1,101 +1,139 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## 4.2.1 and newer
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
See GitHub Releases:
- https://github.com/pylast/pylast/releases
## [4.2.0] - 2021-03-14
## Changed
- Fix unsafe creation of temp file for caching, and improve exception raising (#356)
@kvanzuijlen
- [pre-commit.ci] pre-commit autoupdate (#362) @pre-commit-ci
## [4.1.0] - 2021-01-04
## [4.0.0] - 2020-10-07
## Added ## Added
* Add support for Python 3.9 (#347) @hugovk - Add support for streaming (#336) @kvanzuijlen
- Add Python 3.9 final to Travis CI (#350) @sheetalsingala
## Changed
- Update copyright year (#360) @hugovk
- Replace Travis CI with GitHub Actions (#352) @hugovk
- [pre-commit.ci] pre-commit autoupdate (#359) @pre-commit-ci
## Fixed
- Set limit to 50 by default, not 1 (#355) @hugovk
## [4.0.0] - 2020-10-07
## Added
- Add support for Python 3.9 (#347) @hugovk
## Removed ## Removed
* Remove deprecated `Artist.get_cover_image`, `User.get_artist_tracks` and `STATUS_TOKEN_ERROR` (#348) @hugovk - Remove deprecated `Artist.get_cover_image`, `User.get_artist_tracks` and
* Drop support for EOL Python 3.5 (#346) @hugovk `STATUS_TOKEN_ERROR` (#348) @hugovk
- Drop support for EOL Python 3.5 (#346) @hugovk
## [3.3.0] - 2020-06-25 ## [3.3.0] - 2020-06-25
### Added ### Added
* `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) @hugovk - Deprecate `Artist.get_cover_image`, they're no longer available from Last.fm (#332)
@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. - `STATUS_TOKEN_ERROR` is deprecated and will be removed in a future version. Use
Use `STATUS_OPERATION_FAILED` instead. `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` - Constants `COVER_SMALL`, `COVER_MEDIUM`, `COVER_LARGE`, `COVER_EXTRA_LARGE` and
and `COVER_MEGA`. Use `SIZE_SMALL` etc. instead. ([#282]) `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.1.0]: https://github.com/pylast/pylast/compare/4.0.0...4.1.0
[4.0.0]: https://github.com/pylast/pylast/compare/3.3.0...4.0.0 [4.0.0]: https://github.com/pylast/pylast/compare/3.3.0...4.0.0
[3.3.0]: https://github.com/pylast/pylast/compare/3.2.1...3.3.0 [3.3.0]: https://github.com/pylast/pylast/compare/3.2.1...3.3.0
[3.2.1]: https://github.com/pylast/pylast/compare/3.2.0...3.2.1 [3.2.1]: https://github.com/pylast/pylast/compare/3.2.0...3.2.1

View file

@ -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.

154
README.md
View file

@ -1,61 +1,65 @@
pyLast # pyLast
======
[![PyPI version](https://img.shields.io/pypi/v/pylast.svg)](https://pypi.org/project/pylast/) [![PyPI version](https://img.shields.io/pypi/v/pylast.svg)](https://pypi.org/project/pylast/)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/pylast.svg)](https://pypi.org/project/pylast/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/pylast.svg)](https://pypi.org/project/pylast/)
[![PyPI downloads](https://img.shields.io/pypi/dm/pylast.svg)](https://pypistats.org/packages/pylast) [![PyPI downloads](https://img.shields.io/pypi/dm/pylast.svg)](https://pypistats.org/packages/pylast)
[![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions) [![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions)
[![Coverage (Codecov)](https://codecov.io/gh/pylast/pylast/branch/master/graph/badge.svg)](https://codecov.io/gh/pylast/pylast) [![Coverage (Codecov)](https://codecov.io/gh/pylast/pylast/branch/main/graph/badge.svg)](https://codecov.io/gh/pylast/pylast)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Code style: Black](https://img.shields.io/badge/code%20style-Black-000000.svg)](https://github.com/psf/black)
[![DOI](https://zenodo.org/badge/7803088.svg)](https://zenodo.org/badge/latestdoi/7803088) [![DOI](https://zenodo.org/badge/7803088.svg)](https://zenodo.org/badge/latestdoi/7803088)
A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites such as [Libre.fm](https://libre.fm/). A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites
such as [Libre.fm](https://libre.fm/).
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:
pip install pylast
Install latest development version: Install latest development version:
pip install -U git+https://github.com/pylast/pylast ```sh
python3 -m pip install -U git+https://git.hirad.it/Hirad/pylast
```
Or from requirements.txt: Or from requirements.txt:
-e git://github.com/pylast/pylast.git#egg=pylast ```txt
-e https://git.hirad.it/Hirad/pylast#egg=pylast
```
Note: Note:
* pyLast 4.0.0+ supports Python 3.6-3.9. - pyLast 5.3+ supports Python 3.8-3.13.
* pyLast 3.2.0 - 3.3.0 supports Python 3.5-3.8. - pyLast 5.2+ supports Python 3.8-3.12.
* pyLast 3.0.0 - 3.1.0 supports Python 3.5-3.7. - pyLast 5.1 supports Python 3.7-3.11.
* pyLast 2.2.0 - 2.4.0 supports Python 2.7.10+, 3.4-3.7. - pyLast 5.0 supports Python 3.7-3.10.
* pyLast 2.0.0 - 2.1.0 supports Python 2.7.10+, 3.4-3.6. - pyLast 4.3 - 4.5 supports Python 3.6-3.10.
* pyLast 1.7.0 - 1.9.0 supports Python 2.7, 3.3-3.6. - pyLast 4.0 - 4.2 supports Python 3.6-3.9.
* pyLast 1.0.0 - 1.6.0 supports Python 2.7, 3.3-3.4. - pyLast 3.2 - 3.3 supports Python 3.5-3.8.
* pyLast 0.5 supports Python 2, 3. - pyLast 3.0 - 3.1 supports Python 3.5-3.7.
* pyLast < 0.5 supports Python 2. - pyLast 2.2 - 2.4 supports Python 2.7.10+, 3.4-3.7.
- 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
--------------- pyLast, you need a `Network` object which represents a social music network that is
Last.fm or any other API-compatible one. You can obtain a pre-configured one for Last.fm
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 Last.fm or any other API-compatible one. You can obtain a pre-configured one for Last.fm and use it as follows: and use it as follows:
```python ```python
import pylast import pylast
@ -69,14 +73,50 @@ API_SECRET = "425b55975eed76058ac220b7b4e8a054"
username = "your_user_name" username = "your_user_name"
password_hash = pylast.md5("your_password") password_hash = pylast.md5("your_password")
network = pylast.LastFMNetwork(api_key=API_KEY, api_secret=API_SECRET, network = pylast.LastFMNetwork(
username=username, password_hash=password_hash) api_key=API_KEY,
api_secret=API_SECRET,
username=username,
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"))
@ -85,14 +125,20 @@ track.add_tags(("awesome", "favorite"))
# to get more help about anything and see examples of how it works # to get more help about anything and see examples of how it works
``` ```
More examples in <a href="https://github.com/hugovk/lastfm-tools">hugovk/lastfm-tools</a> and [tests/](tests/). More examples in
<a href="https://github.com/hugovk/lastfm-tools">hugovk/lastfm-tools</a> and
[tests/](https://github.com/pylast/pylast/tree/main/tests).
Testing ## Testing
-------
The [tests/](tests/) directory contains integration and unit tests with Last.fm, and plenty of code examples. The [tests/](https://github.com/pylast/pylast/tree/main/tests) directory contains
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 test data, and an API key and secret. Either copy [example_test_pylast.yaml](example_test_pylast.yaml) to test_pylast.yaml and fill out the credentials, or set them as environment variables like: For integration tests you need a test account at Last.fm that will become cluttered with
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)
to test_pylast.yaml and fill out the credentials, or set them as environment variables
like:
```sh ```sh
export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE
@ -102,17 +148,20 @@ export PYLAST_API_SECRET=TODO_ENTER_YOURS_HERE
``` ```
To run all unit and integration tests: To run all unit and integration tests:
```sh ```sh
pip install -e ".[tests]" python3 -m pip install -e ".[tests]"
pytest pytest
``` ```
Or run just one test case: Or run just one test case:
```sh ```sh
pytest -k test_scrobble pytest -k test_scrobble
``` ```
To run with coverage: To run with coverage:
```sh ```sh
pytest -v --cov pylast --cov-report term-missing pytest -v --cov pylast --cov-report term-missing
coverage report # for command-line report coverage report # for command-line report
@ -120,8 +169,7 @@ 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:
@ -129,7 +177,8 @@ To enable from your own code:
import logging import logging
import pylast import pylast
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.INFO)
network = pylast.LastFMNetwork(...) network = pylast.LastFMNetwork(...)
``` ```
@ -137,5 +186,8 @@ network = pylast.LastFMNetwork(...)
To enable from pytest: To enable from pytest:
```sh ```sh
pytest --log-cli-level debug -k test_album_search_images pytest --log-cli-level info -k test_album_search_images
``` ```
To also see data returned from the API, use `level=logging.DEBUG` or
`--log-cli-level debug` instead.

View file

@ -1,23 +1,22 @@
# Release Checklist # Release Checklist
* [ ] Get master to the appropriate code release state. - [ ] Get `main` to the appropriate code release state.
[GitHub Actions](https://github.com/pylast/pylast/actions) should be running cleanly for [GitHub Actions](https://github.com/pylast/pylast/actions) should be running
all merges to master. cleanly for all merges to `main`.
[![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions) [![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions)
* [ ] Edit release draft, adjust text if needed: - [ ] Edit release draft, adjust text if needed:
https://github.com/pylast/pylast/releases https://github.com/pylast/pylast/releases
* [ ] Check next tag is correct, amend if needed - [ ] Check next tag is correct, amend if needed
* [ ] Copy text into [`CHANGELOG.md`](CHANGELOG.md) - [ ] Publish release
* [ ] Publish release - [ ] Check the tagged
[GitHub Actions build](https://github.com/pylast/pylast/actions/workflows/deploy.yml)
* [ ] Check the tagged [GitHub Actions build](https://github.com/pylast/pylast/actions?query=workflow%3ADeploy)
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__)"

View file

@ -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

97
pyproject.toml Normal file
View file

@ -0,0 +1,97 @@
[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"

View file

@ -2,3 +2,5 @@
filterwarnings = filterwarnings =
once::DeprecationWarning once::DeprecationWarning
once::PendingDeprecationWarning once::PendingDeprecationWarning
xfail_strict=true

View file

@ -1,5 +0,0 @@
[flake8]
max_line_length = 88
[tool:isort]
profile = black

View file

@ -1,46 +0,0 @@
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

View file

@ -2,13 +2,15 @@
""" """
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): def test_album_tags_are_topitems(self) -> None:
# Arrange # Arrange
album = self.network.get_album("Test Artist", "Test Album") album = self.network.get_album("Test Artist", "Test Album")
@ -19,14 +21,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): def test_album_is_hashable(self) -> None:
# 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): def test_album_in_recent_tracks(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
@ -37,7 +39,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
# Assert # Assert
assert hasattr(track, "album") assert hasattr(track, "album")
def test_album_wiki_content(self): def test_album_wiki_content(self) -> None:
# Arrange # Arrange
album = pylast.Album("Test Artist", "Test Album", self.network) album = pylast.Album("Test Artist", "Test Album", self.network)
@ -48,7 +50,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): def test_album_wiki_published_date(self) -> None:
# Arrange # Arrange
album = pylast.Album("Test Artist", "Test Album", self.network) album = pylast.Album("Test Artist", "Test Album", self.network)
@ -59,7 +61,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): def test_album_wiki_summary(self) -> None:
# Arrange # Arrange
album = pylast.Album("Test Artist", "Test Album", self.network) album = pylast.Album("Test Artist", "Test Album", self.network)
@ -70,7 +72,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): def test_album_eq_none_is_false(self) -> None:
# Arrange # Arrange
album1 = None album1 = None
album2 = pylast.Album("Test Artist", "Test Album", self.network) album2 = pylast.Album("Test Artist", "Test Album", self.network)
@ -78,7 +80,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
# Act / Assert # Act / Assert
assert album1 != album2 assert album1 != album2
def test_album_ne_none_is_true(self): def test_album_ne_none_is_true(self) -> None:
# Arrange # Arrange
album1 = None album1 = None
album2 = pylast.Album("Test Artist", "Test Album", self.network) album2 = pylast.Album("Test Artist", "Test Album", self.network)
@ -86,7 +88,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
# Act / Assert # Act / Assert
assert album1 != album2 assert album1 != album2
def test_get_cover_image(self): def test_get_cover_image(self) -> None:
# Arrange # Arrange
album = self.network.get_album("Test Artist", "Test Album") album = self.network.get_album("Test Artist", "Test Album")
@ -94,5 +96,25 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
image = album.get_cover_image() image = album.get_cover_image()
# Assert # Assert
self.assert_startswith(image, "https://") assert image.startswith("https://")
self.assert_endswith(image, ".png") assert image.endswith(".gif") or image.endswith(".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

View file

@ -2,6 +2,8 @@
""" """
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
@ -10,7 +12,7 @@ from .test_pylast import WRITE_TEST, TestPyLastWithLastFm
class TestPyLastArtist(TestPyLastWithLastFm): class TestPyLastArtist(TestPyLastWithLastFm):
def test_repr(self): def test_repr(self) -> None:
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
@ -20,16 +22,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): def test_artist_is_hashable(self) -> None:
# Arrange # Arrange
test_artist = self.network.get_artist("Test Artist") test_artist = self.network.get_artist("Radiohead")
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): def test_bio_published_date(self) -> None:
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
@ -40,7 +42,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): def test_bio_content(self) -> None:
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
@ -51,7 +53,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): def test_bio_content_none(self) -> None:
# 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)
@ -62,7 +64,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
assert bio is None assert bio is None
def test_bio_summary(self): def test_bio_summary(self) -> None:
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
@ -73,7 +75,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): def test_artist_top_tracks(self) -> None:
# 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
@ -84,7 +86,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): def test_artist_top_albums(self) -> None:
# 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
@ -107,7 +109,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
assert len(things) == test_limit assert len(things) == test_limit
def test_artist_top_albums_limit_default(self): def test_artist_top_albums_limit_default(self) -> None:
# 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
@ -118,7 +120,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
assert len(things) == 50 assert len(things) == 50
def test_artist_listener_count(self): def test_artist_listener_count(self) -> None:
# Arrange # Arrange
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
@ -130,7 +132,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): def test_tag_artist(self) -> None:
# Arrange # Arrange
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
# artist.clear_tags() # artist.clear_tags()
@ -145,7 +147,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): def test_remove_tag_of_type_text(self) -> None:
# Arrange # Arrange
tag = "testing" # text tag = "testing" # text
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
@ -160,7 +162,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): def test_remove_tag_of_type_tag(self) -> None:
# 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")
@ -175,7 +177,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): def test_remove_tags(self) -> None:
# Arrange # Arrange
tags = ["removetag1", "removetag2"] tags = ["removetag1", "removetag2"]
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
@ -195,7 +197,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): def test_set_tags(self) -> None:
# Arrange # Arrange
tags = ["sometag1", "sometag2"] tags = ["sometag1", "sometag2"]
artist = self.network.get_artist("Test Artist 2") artist = self.network.get_artist("Test Artist 2")
@ -219,7 +221,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
assert found1 assert found1
assert found2 assert found2
def test_artists(self): def test_artists(self) -> None:
# 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")
@ -229,7 +231,6 @@ 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)
@ -239,9 +240,8 @@ 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): def test_artist_eq_none_is_false(self) -> None:
# 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): def test_artist_ne_none_is_true(self) -> None:
# 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): def test_artist_get_correction(self) -> None:
# Arrange # Arrange
artist = pylast.Artist("guns and roses", self.network) artist = pylast.Artist("guns and roses", self.network)
@ -267,8 +267,7 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
assert corrected_artist_name == "Guns N' Roses" assert corrected_artist_name == "Guns N' Roses"
@pytest.mark.xfail def test_get_userplaycount(self) -> None:
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)
@ -276,4 +275,4 @@ class TestPyLastArtist(TestPyLastWithLastFm):
playcount = artist.get_userplaycount() playcount = artist.get_userplaycount()
# Assert # Assert
assert playcount >= 0 # whilst xfail: # pragma: no cover assert playcount >= 0

View file

@ -2,20 +2,22 @@
""" """
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): def test_country_is_hashable(self) -> None:
# 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): def test_countries(self) -> None:
# 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)

View file

@ -2,13 +2,15 @@
""" """
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): def test_repr(self) -> None:
# Arrange # Arrange
library = pylast.Library(user=self.username, network=self.network) library = pylast.Library(user=self.username, network=self.network)
@ -16,9 +18,9 @@ class TestPyLastLibrary(TestPyLastWithLastFm):
representation = repr(library) representation = repr(library)
# Assert # Assert
self.assert_startswith(representation, "pylast.Library(") assert representation.startswith("pylast.Library(")
def test_str(self): def test_str(self) -> None:
# Arrange # Arrange
library = pylast.Library(user=self.username, network=self.network) library = pylast.Library(user=self.username, network=self.network)
@ -26,23 +28,23 @@ class TestPyLastLibrary(TestPyLastWithLastFm):
string = str(library) string = str(library)
# Assert # Assert
self.assert_endswith(string, "'s Library") assert string.endswith("'s Library")
def test_library_is_hashable(self): def test_library_is_hashable(self) -> None:
# 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): def test_cacheable_library(self) -> None:
# 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): def test_get_user(self) -> None:
# 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)

View file

@ -2,18 +2,20 @@
""" """
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 PyLastTestCase, load_secrets from .test_pylast import load_secrets
@flaky(max_runs=3, min_passes=1) @flaky(max_runs=3, min_passes=1)
class TestPyLastWithLibreFm(PyLastTestCase): class TestPyLastWithLibreFm:
"""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): def test_libre_fm(self) -> None:
# Arrange # Arrange
secrets = load_secrets() secrets = load_secrets()
username = secrets["username"] username = secrets["username"]
@ -27,7 +29,7 @@ class TestPyLastWithLibreFm(PyLastTestCase):
# Assert # Assert
assert name == "Radiohead" assert name == "Radiohead"
def test_repr(self): def test_repr(self) -> None:
# Arrange # Arrange
secrets = load_secrets() secrets = load_secrets()
username = secrets["username"] username = secrets["username"]
@ -38,4 +40,4 @@ class TestPyLastWithLibreFm(PyLastTestCase):
representation = repr(network) representation = repr(network)
# Assert # Assert
self.assert_startswith(representation, "pylast.LibreFMNetwork(") assert representation.startswith("pylast.LibreFMNetwork(")

View file

@ -1,7 +1,9 @@
#!/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
@ -14,7 +16,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): def test_scrobble(self) -> None:
# Arrange # Arrange
artist = "test artist" artist = "test artist"
title = "test title" title = "test title"
@ -32,7 +34,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): def test_update_now_playing(self) -> None:
# Arrange # Arrange
artist = "Test Artist" artist = "Test Artist"
title = "test title" title = "test title"
@ -56,7 +58,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): def test_enable_rate_limiting(self) -> None:
# Arrange # Arrange
assert not self.network.is_rate_limited() assert not self.network.is_rate_limited()
@ -73,7 +75,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): def test_disable_rate_limiting(self) -> None:
# Arrange # Arrange
self.network.enable_rate_limit() self.network.enable_rate_limit()
assert self.network.is_rate_limited() assert self.network.is_rate_limited()
@ -88,14 +90,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): def test_lastfm_network_name(self) -> None:
# 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): def test_geo_get_top_artists(self) -> None:
# 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)
@ -105,7 +107,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): def test_geo_get_top_tracks(self) -> None:
# Arrange # Arrange
# Act # Act
tracks = self.network.get_geo_top_tracks( tracks = self.network.get_geo_top_tracks(
@ -117,7 +119,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): def test_network_get_top_artists_with_limit(self) -> None:
# Arrange # Arrange
# Act # Act
artists = self.network.get_top_artists(limit=1) artists = self.network.get_top_artists(limit=1)
@ -125,7 +127,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): def test_network_get_top_tags_with_limit(self) -> None:
# Arrange # Arrange
# Act # Act
tags = self.network.get_top_tags(limit=1) tags = self.network.get_top_tags(limit=1)
@ -133,7 +135,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): def test_network_get_top_tags_with_no_limit(self) -> None:
# Arrange # Arrange
# Act # Act
tags = self.network.get_top_tags() tags = self.network.get_top_tags()
@ -141,7 +143,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): def test_network_get_top_tracks_with_limit(self) -> None:
# Arrange # Arrange
# Act # Act
tracks = self.network.get_top_tracks(limit=1) tracks = self.network.get_top_tracks(limit=1)
@ -149,7 +151,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): def test_country_top_tracks(self) -> None:
# Arrange # Arrange
country = self.network.get_country("Croatia") country = self.network.get_country("Croatia")
@ -159,7 +161,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): def test_country_network_top_tracks(self) -> None:
# Arrange # Arrange
# Act # Act
things = self.network.get_geo_top_tracks("Croatia", limit=2) things = self.network.get_geo_top_tracks("Croatia", limit=2)
@ -167,7 +169,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): def test_tag_top_tracks(self) -> None:
# Arrange # Arrange
tag = self.network.get_tag("blues") tag = self.network.get_tag("blues")
@ -177,7 +179,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): def test_album_data(self) -> None:
# Arrange # Arrange
thing = self.network.get_album("Test Artist", "Test Album") thing = self.network.get_album("Test Artist", "Test Album")
@ -197,7 +199,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): def test_track_data(self) -> None:
# Arrange # Arrange
thing = self.network.get_track("Test Artist", "test title") thing = self.network.get_track("Test Artist", "test title")
@ -218,7 +220,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): def test_country_top_artists(self) -> None:
# Arrange # Arrange
country = self.network.get_country("Ukraine") country = self.network.get_country("Ukraine")
@ -228,7 +230,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): def test_caching(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -243,9 +245,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): def test_album_mbid(self) -> None:
# Arrange # Arrange
mbid = "a6a265bf-9f81-4055-8224-f7ac0aa6b937" mbid = "03c91c40-49a6-44a7-90e7-a700edf97a62"
# Act # Act
album = self.network.get_album_by_mbid(mbid) album = self.network.get_album_by_mbid(mbid)
@ -253,10 +255,10 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
assert isinstance(album, pylast.Album) assert isinstance(album, pylast.Album)
assert album.title.lower() == "test" assert album.title == "Believe"
assert album_mbid == mbid assert album_mbid == mbid
def test_artist_mbid(self): def test_artist_mbid(self) -> None:
# Arrange # Arrange
mbid = "7e84f845-ac16-41fe-9ff8-df12eb32af55" mbid = "7e84f845-ac16-41fe-9ff8-df12eb32af55"
@ -265,9 +267,9 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
assert isinstance(artist, pylast.Artist) assert isinstance(artist, pylast.Artist)
assert artist.name == "MusicBrainz Test Artist" assert artist.name in ("MusicBrainz Test Artist", "MusicBrainzz Test Artist")
def test_track_mbid(self): def test_track_mbid(self) -> None:
# Arrange # Arrange
mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0" mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0"
@ -280,7 +282,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): def test_init_with_token(self) -> None:
# Arrange/Act # Arrange/Act
msg = None msg = None
try: try:
@ -295,20 +297,19 @@ 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): def test_proxy(self) -> None:
# Arrange # Arrange
host = "https://example.com" proxy = "http://example.com:1234"
port = 1234
# Act / Assert # Act / Assert
self.network.enable_proxy(host, port) self.network.enable_proxy(proxy)
assert self.network.is_proxy_enabled() assert self.network.is_proxy_enabled()
assert self.network._get_proxy() == ["https://example.com", 1234] assert self.network.proxy == "http://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): def test_album_search(self) -> None:
# Arrange # Arrange
album = "Nevermind" album = "Nevermind"
@ -320,7 +321,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): def test_album_search_images(self) -> None:
# Arrange # Arrange
album = "Nevermind" album = "Nevermind"
search = self.network.search_for_album(album) search = self.network.search_for_album(album)
@ -332,15 +333,15 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
assert len(images) == 4 assert len(images) == 4
self.assert_startswith(images[pylast.SIZE_SMALL], "https://") assert images[pylast.SIZE_SMALL].startswith("https://")
self.assert_endswith(images[pylast.SIZE_SMALL], ".png") assert images[pylast.SIZE_SMALL].endswith(".png")
assert "/34s/" in images[pylast.SIZE_SMALL] assert "/34s/" in images[pylast.SIZE_SMALL]
self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://") assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://")
self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png")
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE]
def test_artist_search(self): def test_artist_search(self) -> None:
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
@ -352,7 +353,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): def test_artist_search_images(self) -> None:
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
search = self.network.search_for_artist(artist) search = self.network.search_for_artist(artist)
@ -364,15 +365,15 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
assert len(images) == 5 assert len(images) == 5
self.assert_startswith(images[pylast.SIZE_SMALL], "https://") assert images[pylast.SIZE_SMALL].startswith("https://")
self.assert_endswith(images[pylast.SIZE_SMALL], ".png") assert images[pylast.SIZE_SMALL].endswith(".png")
assert "/34s/" in images[pylast.SIZE_SMALL] assert "/34s/" in images[pylast.SIZE_SMALL]
self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://") assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://")
self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png")
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE]
def test_track_search(self): def test_track_search(self) -> None:
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
track = "Smells Like Teen Spirit" track = "Smells Like Teen Spirit"
@ -385,7 +386,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): def test_track_search_images(self) -> None:
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
track = "Smells Like Teen Spirit" track = "Smells Like Teen Spirit"
@ -398,15 +399,15 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
assert len(images) == 4 assert len(images) == 4
self.assert_startswith(images[pylast.SIZE_SMALL], "https://") assert images[pylast.SIZE_SMALL].startswith("https://")
self.assert_endswith(images[pylast.SIZE_SMALL], ".png") assert images[pylast.SIZE_SMALL].endswith(".png")
assert "/34s/" in images[pylast.SIZE_SMALL] assert "/34s/" in images[pylast.SIZE_SMALL]
self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://") assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://")
self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") assert images[pylast.SIZE_EXTRA_LARGE].endswith(".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): def test_search_get_total_result_count(self) -> None:
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
track = "Smells Like Teen Spirit" track = "Smells Like Teen Spirit"

View file

@ -2,8 +2,9 @@
""" """
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
@ -11,7 +12,7 @@ from flaky import flaky
import pylast import pylast
WRITE_TEST = sys.version_info[:2] == (3, 8) WRITE_TEST = False
def load_secrets(): # pragma: no cover def load_secrets(): # pragma: no cover
@ -33,29 +34,21 @@ def load_secrets(): # pragma: no cover
return doc return doc
class PyLastTestCase: def _no_xfail_rerun_filter(err, name, test, plugin) -> bool:
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(PyLastTestCase): class TestPyLastWithLastFm:
secrets = None secrets = None
def unix_timestamp(self): @staticmethod
def unix_timestamp() -> int:
return int(time.time()) return int(time.time())
@classmethod @classmethod
def setup_class(cls): def setup_class(cls) -> None:
if cls.secrets is None: if cls.secrets is None:
cls.secrets = load_secrets() cls.secrets = load_secrets()
@ -72,7 +65,8 @@ class TestPyLastWithLastFm(PyLastTestCase):
password_hash=password_hash, password_hash=password_hash,
) )
def helper_is_thing_hashable(self, thing): @staticmethod
def helper_is_thing_hashable(thing) -> None:
# Arrange # Arrange
things = set() things = set()
@ -83,7 +77,8 @@ class TestPyLastWithLastFm(PyLastTestCase):
assert thing is not None assert thing is not None
assert len(things) == 1 assert len(things) == 1
def helper_validate_results(self, a, b, c): @staticmethod
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
@ -94,7 +89,7 @@ class TestPyLastWithLastFm(PyLastTestCase):
assert a == b assert a == b
assert b == c assert b == c
def helper_validate_cacheable(self, thing, function_name): def helper_validate_cacheable(self, thing, function_name) -> None:
# Arrange # Arrange
# get thing.function_name() # get thing.function_name()
func = getattr(thing, function_name, None) func = getattr(thing, function_name, None)
@ -107,27 +102,31 @@ class TestPyLastWithLastFm(PyLastTestCase):
# Assert # Assert
self.helper_validate_results(result1, result2, result3) self.helper_validate_results(result1, result2, result3)
def helper_at_least_one_thing_in_top_list(self, things, expected_type): @staticmethod
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)
def helper_only_one_thing_in_top_list(self, things, expected_type): @staticmethod
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)
def helper_only_one_thing_in_list(self, things, expected_type): @staticmethod
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)
def helper_two_different_things_in_top_list(self, things, expected_type): @staticmethod
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]

View file

@ -2,20 +2,22 @@
""" """
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): def test_tag_is_hashable(self) -> None:
# 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): def test_tag_top_artists(self) -> None:
# Arrange # Arrange
tag = self.network.get_tag("blues") tag = self.network.get_tag("blues")
@ -25,7 +27,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): def test_tag_top_albums(self) -> None:
# Arrange # Arrange
tag = self.network.get_tag("blues") tag = self.network.get_tag("blues")
@ -35,7 +37,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): def test_tags(self) -> None:
# 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")

View file

@ -1,7 +1,9 @@
#!/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
@ -13,7 +15,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): def test_love(self) -> None:
# Arrange # Arrange
artist = "Test Artist" artist = "Test Artist"
title = "test title" title = "test title"
@ -29,7 +31,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): def test_unlove(self) -> None:
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
title = "test title" title = "test title"
@ -47,7 +49,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): def test_user_play_count_in_track_info(self) -> None:
# Arrange # Arrange
artist = "Test Artist" artist = "Test Artist"
title = "test title" title = "test title"
@ -61,7 +63,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
# Assert # Assert
assert count >= 0 assert count >= 0
def test_user_loved_in_track_info(self): def test_user_loved_in_track_info(self) -> None:
# Arrange # Arrange
artist = "Test Artist" artist = "Test Artist"
title = "test title" title = "test title"
@ -77,7 +79,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): def test_track_is_hashable(self) -> None:
# 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
@ -86,7 +88,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): def test_track_wiki_content(self) -> None:
# Arrange # Arrange
track = pylast.Track("Test Artist", "test title", self.network) track = pylast.Track("Test Artist", "test title", self.network)
@ -97,7 +99,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): def test_track_wiki_summary(self) -> None:
# Arrange # Arrange
track = pylast.Track("Test Artist", "test title", self.network) track = pylast.Track("Test Artist", "test title", self.network)
@ -108,37 +110,17 @@ 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): def test_track_get_duration(self) -> None:
# Arrange # Arrange
track = pylast.Track("Nirvana", "Lithium", self.network) track = pylast.Track("Daft Punk", "Something About Us", self.network)
# Act # Act
duration = track.get_duration() duration = track.get_duration()
# Assert # Assert
assert duration >= 200000 assert duration >= 100000
def test_track_is_streamable(self): def test_track_get_album(self) -> None:
# 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)
@ -148,7 +130,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
# Assert # Assert
assert str(album) == "Nirvana - Nevermind" assert str(album) == "Nirvana - Nevermind"
def test_track_get_similar(self): def test_track_get_similar(self) -> None:
# Arrange # Arrange
track = pylast.Track("Cher", "Believe", self.network) track = pylast.Track("Cher", "Believe", self.network)
@ -156,14 +138,10 @@ class TestPyLastTrack(TestPyLastWithLastFm):
similar = track.get_similar() similar = track.get_similar()
# Assert # Assert
found = False found = any(str(track.item) == "Cher - Strong Enough" for track in similar)
for track in similar:
if str(track.item) == "Madonna - Vogue":
found = True
break
assert found assert found
def test_track_get_similar_limits(self): def test_track_get_similar_limits(self) -> None:
# Arrange # Arrange
track = pylast.Track("Cher", "Believe", self.network) track = pylast.Track("Cher", "Believe", self.network)
@ -173,7 +151,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): def test_tracks_notequal(self) -> None:
# 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)
@ -182,7 +160,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
# Assert # Assert
assert track1 != track2 assert track1 != track2
def test_track_title_prop_caps(self): def test_track_title_prop_caps(self) -> None:
# Arrange # Arrange
track = pylast.Track("test artist", "test title", self.network) track = pylast.Track("test artist", "test title", self.network)
@ -192,7 +170,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
# Assert # Assert
assert title == "Test Title" assert title == "Test Title"
def test_track_listener_count(self): def test_track_listener_count(self) -> None:
# Arrange # Arrange
track = pylast.Track("test artist", "test title", self.network) track = pylast.Track("test artist", "test title", self.network)
@ -202,7 +180,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
# Assert # Assert
assert count > 21 assert count > 21
def test_album_tracks(self): def test_album_tracks(self) -> None:
# Arrange # Arrange
album = pylast.Album("Test Artist", "Test", self.network) album = pylast.Album("Test Artist", "Test", self.network)
@ -216,7 +194,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): def test_track_eq_none_is_false(self) -> None:
# Arrange # Arrange
track1 = None track1 = None
track2 = pylast.Track("Test Artist", "test title", self.network) track2 = pylast.Track("Test Artist", "test title", self.network)
@ -224,7 +202,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
# Act / Assert # Act / Assert
assert track1 != track2 assert track1 != track2
def test_track_ne_none_is_true(self): def test_track_ne_none_is_true(self) -> None:
# Arrange # Arrange
track1 = None track1 = None
track2 = pylast.Track("Test Artist", "test title", self.network) track2 = pylast.Track("Test Artist", "test title", self.network)
@ -232,7 +210,7 @@ class TestPyLastTrack(TestPyLastWithLastFm):
# Act / Assert # Act / Assert
assert track1 != track2 assert track1 != track2
def test_track_get_correction(self): def test_track_get_correction(self) -> None:
# Arrange # Arrange
track = pylast.Track("Guns N' Roses", "mrbrownstone", self.network) track = pylast.Track("Guns N' Roses", "mrbrownstone", self.network)
@ -242,7 +220,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): def test_track_with_no_mbid(self) -> None:
# Arrange # Arrange
track = pylast.Track("Static-X", "Set It Off", self.network) track = pylast.Track("Static-X", "Set It Off", self.network)

View file

@ -2,6 +2,8 @@
""" """
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
@ -16,7 +18,7 @@ from .test_pylast import TestPyLastWithLastFm
class TestPyLastUser(TestPyLastWithLastFm): class TestPyLastUser(TestPyLastWithLastFm):
def test_repr(self): def test_repr(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -24,9 +26,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
representation = repr(user) representation = repr(user)
# Assert # Assert
self.assert_startswith(representation, "pylast.User('RJ',") assert representation.startswith("pylast.User('RJ',")
def test_str(self): def test_str(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -36,7 +38,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert string == "RJ" assert string == "RJ"
def test_equality(self): def test_equality(self) -> None:
# 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")
@ -48,7 +50,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): def test_get_name(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -58,7 +60,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert name == "RJ" assert name == "RJ"
def test_get_user_registration(self): def test_get_user_registration(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -74,7 +76,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): def test_get_user_unixtime_registration(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -85,7 +87,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): def test_get_countryless_user(self) -> None:
# 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")
@ -96,7 +98,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert country is None assert country is None
def test_user_get_country(self): def test_user_get_country(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user("RJ") lastfm_user = self.network.get_user("RJ")
@ -106,7 +108,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert str(country) == "United Kingdom" assert str(country) == "United Kingdom"
def test_user_equals_none(self): def test_user_equals_none(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
@ -116,7 +118,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert not value assert not value
def test_user_not_equal_to_none(self): def test_user_not_equal_to_none(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
@ -126,7 +128,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert value assert value
def test_now_playing_user_with_no_scrobbles(self): def test_now_playing_user_with_no_scrobbles(self) -> None:
# 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")
@ -137,7 +139,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert current_track is None assert current_track is None
def test_love_limits(self): def test_love_limits(self) -> None:
# 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")
@ -148,7 +150,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): def test_user_is_hashable(self) -> None:
# Arrange # Arrange
user = self.network.get_user(self.username) user = self.network.get_user(self.username)
@ -169,7 +171,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# # Assert # # Assert
# self.assertGreaterEqual(len(tracks), 0) # self.assertGreaterEqual(len(tracks), 0)
def test_pickle(self): def test_pickle(self) -> None:
# Arrange # Arrange
import pickle import pickle
@ -187,7 +189,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): def test_cacheable_user(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_authenticated_user() lastfm_user = self.network.get_authenticated_user()
@ -201,7 +203,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
lastfm_user, "get_recent_tracks" lastfm_user, "get_recent_tracks"
) )
def test_user_get_top_tags_with_limit(self): def test_user_get_top_tags_with_limit(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -211,7 +213,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): def test_user_top_tracks(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user("RJ") lastfm_user = self.network.get_user("RJ")
@ -221,14 +223,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): def helper_assert_chart(self, chart, expected_type) -> None:
# 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): def helper_get_assert_charts(self, thing, date) -> None:
# Arrange # Arrange
album_chart, track_chart = None, None album_chart, track_chart = None, None
(from_date, to_date) = date (from_date, to_date) = date
@ -245,14 +247,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): def helper_dates_valid(self, dates) -> None:
# 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): def test_user_charts(self) -> None:
# 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()
@ -261,7 +263,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): def test_user_top_artists(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
@ -271,7 +273,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): def test_user_top_albums(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -285,7 +287,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): def test_user_tagged_artists(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
tags = ["artisttagola"] tags = ["artisttagola"]
@ -298,7 +300,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): def test_user_tagged_albums(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
tags = ["albumtagola"] tags = ["albumtagola"]
@ -311,7 +313,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): def test_user_tagged_tracks(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
tags = ["tracktagola"] tags = ["tracktagola"]
@ -324,7 +326,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): def test_user_subscriber(self) -> None:
# 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")
@ -337,7 +339,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): def test_user_get_image(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -345,9 +347,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
url = user.get_image() url = user.get_image()
# Assert # Assert
self.assert_startswith(url, "https://") assert url.startswith("https://")
def test_user_get_library(self): def test_user_get_library(self) -> None:
# Arrange # Arrange
user = self.network.get_user(self.username) user = self.network.get_user(self.username)
@ -357,7 +359,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert isinstance(library, pylast.Library) assert isinstance(library, pylast.Library)
def test_get_recent_tracks_from_to(self): def test_get_recent_tracks_from_to(self) -> None:
# 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)
@ -374,7 +376,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): def test_get_recent_tracks_limit_none(self) -> None:
# 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)
@ -393,7 +395,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): def test_get_recent_tracks_is_streamable(self) -> None:
# 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)
@ -410,7 +412,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert inspect.isgenerator(tracks) assert inspect.isgenerator(tracks)
def test_get_playcount(self): def test_get_playcount(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -420,7 +422,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
assert playcount >= 128387 assert playcount >= 128387
def test_get_image(self): def test_get_image(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -428,10 +430,10 @@ class TestPyLastUser(TestPyLastWithLastFm):
image = user.get_image() image = user.get_image()
# Assert # Assert
self.assert_startswith(image, "https://") assert image.startswith("https://")
self.assert_endswith(image, ".png") assert image.endswith(".png")
def test_get_url(self): def test_get_url(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -441,7 +443,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): def test_get_weekly_artist_charts(self) -> None:
# Arrange # Arrange
user = self.network.get_user("bbc6music") user = self.network.get_user("bbc6music")
@ -453,7 +455,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): def test_get_weekly_track_charts(self) -> None:
# Arrange # Arrange
user = self.network.get_user("bbc6music") user = self.network.get_user("bbc6music")
@ -465,7 +467,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): def test_user_get_track_scrobbles(self) -> None:
# Arrange # Arrange
artist = "France Gall" artist = "France Gall"
title = "Laisse Tomber Les Filles" title = "Laisse Tomber Les Filles"
@ -479,7 +481,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): def test_cacheable_user_get_track_scrobbles(self) -> None:
# Arrange # Arrange
artist = "France Gall" artist = "France Gall"
title = "Laisse Tomber Les Filles" title = "Laisse Tomber Les Filles"

View file

@ -1,3 +1,5 @@
from __future__ import annotations
from unittest import mock from unittest import mock
import pytest import pytest
@ -18,12 +20,51 @@ def mock_network():
"fdasfdsafsaf not unicode", "fdasfdsafsaf not unicode",
], ],
) )
def test_get_cache_key(artist): def test_get_cache_key(artist) -> None:
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): def test_cast_and_hash(obj) -> None:
assert type(str(obj)) is str assert isinstance(str(obj), 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
View file

@ -1,29 +1,40 @@
[tox] [tox]
envlist = requires =
py{py3, 310, 39, 38, 37, 36} tox>=4.2
env_list =
lint
py{py3, 313, 312, 311, 310, 39, 38}
[testenv] [testenv]
passenv = extras =
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 =
pytest -v -s -W all --cov pylast --cov tests --cov-report term-missing --random-order {posargs} {envpython} -m pytest -v -s -W all \
--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