Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 511986a131 | |||
| 8dc88f6361 | |||
| 0a77fa34fd | |||
| 0034340d63 | |||
| 02cb79c4b2 | |||
| 26b346d359 | |||
| 1dae95735f | |||
| 78544673b4 | |||
| e8ce4b59f8 | |||
| e09728a7c7 | |||
| ae6c2bf985 | |||
| fd144abff0 | |||
| d499ee175e | |||
| fadcb98d81 | |||
| aa98102d6a | |||
| a10426f043 | |||
| 78ac63ca36 | |||
| 2d8f8dc63f | |||
| 62413ce829 | |||
| 52e1d3f8db | |||
| 06368aa532 | |||
| 9d59a8f4fb | |||
| 237be15ecd | |||
| bc4d939eff | |||
| ed9a30db13 | |||
| 10a46516d5 | |||
| 15595892ec | |||
| 13188c38b9 | |||
| 8fcb0d8704 | |||
| 3fecf9d723 | |||
| 76a5d580ab | |||
| a322df759e | |||
| f06c9f43d4 | |||
| 199302afb3 | |||
| d6e7e49f3f | |||
| c711795cce | |||
| 026bfc12b7 | |||
| 06a42afb2f | |||
| 4d5b32bcd9 | |||
| 509cd86830 | |||
| f2145d8cdc | |||
| d89c738c97 | |||
| 263cf33cf6 | |||
| 62111cfe63 | |||
| 5a1e4e72a1 | |||
| 74c18b50b9 | |||
| 0d64bf7b83 | |||
| c9632c129a | |||
| 1f16f3b433 | |||
| ff155f81f4 | |||
| 383aa2b77b | |||
| a431ce60b7 | |||
| 4f7b829c96 | |||
| ac0a1f6709 | |||
| 7214a43421 | |||
| 1af282be47 | |||
| d3fc96cfdd | |||
| a73a34f1a7 | |||
| bc0b41fae3 | |||
| 3ec71d7076 | |||
| 805067521f | |||
| 7900da7299 | |||
| 3d1aeaed3c | |||
| eed16b9128 | |||
| 76fe7333db | |||
| 6456e3645e | |||
| 19e706eb50 | |||
|
|
94476113da | ||
|
|
2d792a4a50 | ||
|
|
c0841a0035 | ||
|
|
2af1cc68f3 | ||
|
|
6a3c09ca21 | ||
| be8ce7edcb | |||
| d2b43414a4 | |||
| 50e9ce6e2e | |||
|
|
36686c668d | ||
|
|
14da065144 |
21
CHANGELOG.rst
Normal file
21
CHANGELOG.rst
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Changelog
|
||||||
|
==========
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
This format is based on `Keep a Changelog`_
|
||||||
|
|
||||||
|
.. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/
|
||||||
|
|
||||||
|
and this project adheres to `Semantic Versioning`_
|
||||||
|
|
||||||
|
.. _Semantic Versioning: https://semver.org/spec/v2.0.0.html
|
||||||
|
|
||||||
|
|
||||||
|
[1.0.0] - 2020-03-07
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Added
|
||||||
|
######
|
||||||
|
|
||||||
|
- Initial version
|
||||||
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM python:3.7.6-slim-buster
|
||||||
|
|
||||||
|
RUN mkdir /outputs && mkdir /inputs
|
||||||
|
|
||||||
|
COPY ./setup.py /inputs/setup.py
|
||||||
|
COPY ./src /inputs/src
|
||||||
|
|
||||||
|
WORKDIR /inputs
|
||||||
|
RUN python ./setup.py install
|
||||||
151
README.rst
151
README.rst
@@ -0,0 +1,151 @@
|
|||||||
|
=============
|
||||||
|
Introduction
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/readthedocs/panaetius?style=for-the-badge
|
||||||
|
.. image:: https://img.shields.io/github/v/tag/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
|
.. image:: https://img.shields.io/github/commit-activity/m/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
|
.. image:: https://img.shields.io/github/issues/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
|
.. image:: https://img.shields.io/github/license/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
|
.. image:: https://img.shields.io/github/languages/code-size/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
|
.. image:: https://img.shields.io/github/languages/top/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
|
.. image:: https://img.shields.io/requires/github/dtomlinson91/musicbrainzapi-cv-airelogic?style=for-the-badge
|
||||||
|
|
||||||
|
Summary
|
||||||
|
========
|
||||||
|
|
||||||
|
Musicbrainzapi is a Python module with a CLI that allows you to search for an artist and receive summary statistics on lyrics across all albums + tracks.
|
||||||
|
|
||||||
|
The module can also generate and display a wordcloud from the lyrics.
|
||||||
|
|
||||||
|
In addition to basic statistics the module further allows you to save details of an artist. You can save album information, the lyrics themselves and track lists.
|
||||||
|
|
||||||
|
The module (currently) provides a simple CLI with some underlying assumptions:
|
||||||
|
|
||||||
|
- We are interested in albums only - no singles.
|
||||||
|
- We are interested in any album where the artist is listed as a primary artist on a release. This could include compilations or joint albums with other artists.
|
||||||
|
- Where an album has been released multiple times, in different regions, we take the album with the longest track list.
|
||||||
|
|
||||||
|
These assumptions are not configurable in the current version - but this functionality could be added to the module if needed.
|
||||||
|
|
||||||
|
Further information, and a brief summary of decisions taken and current caveats can be found in the documentation which is linked below.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
=============
|
||||||
|
|
||||||
|
The documentation for the module can be found at https://musicbrainzapi-cv-airelogic.readthedocs.io/en/latest/
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
You will need ``python>=3.7``. Installation to a python virtual environment is recommended.
|
||||||
|
|
||||||
|
PIP
|
||||||
|
---
|
||||||
|
|
||||||
|
Download the latest release ``.whl`` file from the `releases`_ page
|
||||||
|
|
||||||
|
.. _releases: https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic/releases
|
||||||
|
|
||||||
|
In a virtual environment run:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install -U musicbrainzapi.whl
|
||||||
|
|
||||||
|
Replacing ``musicbrainzapi.whl`` with the filename you downloaded.
|
||||||
|
|
||||||
|
setup.py
|
||||||
|
--------
|
||||||
|
|
||||||
|
Clone the repo:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
git clone https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic.git
|
||||||
|
|
||||||
|
In the root of the repo in a virtual environment run:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
python ./setup.py install
|
||||||
|
|
||||||
|
poetry
|
||||||
|
------
|
||||||
|
|
||||||
|
Clone the repo:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
git clone https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic.git
|
||||||
|
|
||||||
|
In a virtual environment install poetry:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install poetry
|
||||||
|
|
||||||
|
In the root of the repo in a virtual environment run:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
poetry install --no-dev
|
||||||
|
|
||||||
|
Docker
|
||||||
|
------
|
||||||
|
|
||||||
|
.. note:: Using Docker will mean you cannot view a wordcloud, as the default behaviour is to show the plot interactively which the container cannot do.
|
||||||
|
|
||||||
|
If you don't have ``python>=3.7`` installed, or would rather use Docker, you can build a Docker image and run the module using Docker.
|
||||||
|
|
||||||
|
Clone the repo:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
git clone https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic.git
|
||||||
|
|
||||||
|
In the root of the repo build the Docker image:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
docker build . -t musicbrainzapi
|
||||||
|
|
||||||
|
No entrypoint is provided in the ``Dockerfile`` - you will have to specify the command at runtime and run the container in interactive mode:
|
||||||
|
|
||||||
|
Using Docker run
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
docker run --rm -it --volume=$(pwd):/outputs \
|
||||||
|
musicbrainzapi:latest musicbrainzapi --path /outputs \
|
||||||
|
lyrics -a "Savage Garden" -c gb --show-summary all --save-output
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
Once installed you can access the command running:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
musicbrainzapi
|
||||||
|
|
||||||
|
To see all options available you can run:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
musicbrainzapi --help
|
||||||
|
|
||||||
|
In the current release there is one command available ``lyrics``:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
musicbrainzapi lyrics --help
|
||||||
|
|
||||||
|
License information
|
||||||
|
===================
|
||||||
|
|
||||||
|
Released under the `MIT License`_
|
||||||
|
|
||||||
|
.. _MIT License: https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic/blob/master/LICENSE
|
||||||
|
|||||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = source
|
||||||
|
BUILDDIR = build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set SOURCEDIR=source
|
||||||
|
set BUILDDIR=build
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.http://sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:help
|
||||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
|
||||||
|
:end
|
||||||
|
popd
|
||||||
48
docs/source/CLI.rst
Normal file
48
docs/source/CLI.rst
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
***
|
||||||
|
CLI
|
||||||
|
***
|
||||||
|
|
||||||
|
As the CLI is provided by `Click`_ , you can pass the ``--help`` option to the base command, or any subcommands, to see information on usage and all available options.
|
||||||
|
|
||||||
|
.. _Click: https://click.palletsprojects.com/en/7.x/
|
||||||
|
|
||||||
|
Full options of the CLI are provided on this page.
|
||||||
|
|
||||||
|
.. important:: The ``--path`` option should be provided to the base command. This is so the path provided can be used in all subcommands.
|
||||||
|
|
||||||
|
|
||||||
|
Quickstart
|
||||||
|
==========
|
||||||
|
|
||||||
|
If you want to see everything the module offers run the following:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
musicbrainzapi --path . lyrics -a "savage garden" -c gb --show-summary all --wordcloud --save-output
|
||||||
|
|
||||||
|
This will search for all tracks across all albums for the artist Savage Garden.
|
||||||
|
|
||||||
|
``--show-summary all`` will show descriptive statistics for both albums and years for this artist.
|
||||||
|
|
||||||
|
``--wordcloud`` will generate a wordcloud showing the most popular words across all lyrics.
|
||||||
|
|
||||||
|
``--save-output`` will save the module's output to disk as ``.json`` files.
|
||||||
|
|
||||||
|
Outputs
|
||||||
|
=======
|
||||||
|
|
||||||
|
The following files will be saved to disk
|
||||||
|
|
||||||
|
- all_albums_lyrics_sum.json - Total number of words in a track for each album.
|
||||||
|
- year_statistics.json - Descriptive statistics by year.
|
||||||
|
- album_statistics.json - Descriptive statistics by album
|
||||||
|
- all_albums_with_tracks.json - Track titles for each album.
|
||||||
|
- all_albums_with_lyrics.json - Lyrics for each track for each album.
|
||||||
|
- all_albums_lyrics_count.json - Shows a frequency count of each word in every track.
|
||||||
|
|
||||||
|
CLI Documentation
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. click:: musicbrainzapi.cli.cli:cli
|
||||||
|
:prog: musicbrainzapi
|
||||||
|
:show-nested:
|
||||||
5
docs/source/_static/custom.css
Normal file
5
docs/source/_static/custom.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@import url("css/theme.css");
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
background: white !important
|
||||||
|
}
|
||||||
1
docs/source/changelog.rst
Normal file
1
docs/source/changelog.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.. include:: ../../CHANGELOG.rst
|
||||||
126
docs/source/comments.rst
Normal file
126
docs/source/comments.rst
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
***********************
|
||||||
|
Comments + Improvements
|
||||||
|
***********************
|
||||||
|
|
||||||
|
Python packages
|
||||||
|
===============
|
||||||
|
|
||||||
|
In this project we use the following Python packages:
|
||||||
|
|
||||||
|
+----------------+-------------------------------------------------------------------------+
|
||||||
|
| musicbrainzngs | This is a python wrapper around the Musicbrainz api. |
|
||||||
|
| | This was used primarily to save time - the module handles all |
|
||||||
|
| | the endpoints and it provides checks to make sure variables |
|
||||||
|
| | passed are valid for the api. Behind the scenes it is using |
|
||||||
|
| | the requests library, and parsing the output into a python dict. |
|
||||||
|
+----------------+-------------------------------------------------------------------------+
|
||||||
|
| addict | The addict library gives us a Dict class. This is a personal preference |
|
||||||
|
| | but I find the syntax easier to work with than standard python when |
|
||||||
|
| | dealing with many dictionaries. It is just subclass of the default |
|
||||||
|
| | ``dict`` class. |
|
||||||
|
+----------------+-------------------------------------------------------------------------+
|
||||||
|
| numpy | One of the best python libraries - it gives us easy access to quantiles |
|
||||||
|
| | and other basic stats. |
|
||||||
|
+----------------+-------------------------------------------------------------------------+
|
||||||
|
| beautifultable | Prints nice tables to stdout. Useful for showing data with a CLI. |
|
||||||
|
+----------------+-------------------------------------------------------------------------+
|
||||||
|
| wordcloud | The best library (I've found) for generating wordclouds. |
|
||||||
|
+----------------+-------------------------------------------------------------------------+
|
||||||
|
| click | I personally prefer click over alternatives like Cleo. This is used |
|
||||||
|
| | to provide the framework for the CLI. |
|
||||||
|
+----------------+-------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
Caveats
|
||||||
|
=======
|
||||||
|
|
||||||
|
The lyrics.ovh api requires the artist to match exactly what it has on record - it will not do any parsing to try look for similar matches. An example of this can be seen with the band "The All‐American Rejects". Musicbrainz returns the band with the "-", but the lyrics.ovh api requires a space character instead.
|
||||||
|
|
||||||
|
A solution to this would be to filter the artist name if it contains any of these characters. But without thorough testing I did not implement this - as it could break other artists.
|
||||||
|
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
============
|
||||||
|
|
||||||
|
Although fully (as far as I have tested) functional - the module could be improved several ways.
|
||||||
|
|
||||||
|
Testing
|
||||||
|
-------
|
||||||
|
|
||||||
|
Implementing a thorough test suite using ``pytest`` and ``coverage`` would be beneficial. Changes to the way the module parses data could be made with confidence if testing were implemented. As the data returned from Musicbrainz publishes a schema, this could be used to implement tests to make sure the code is fully covered.
|
||||||
|
|
||||||
|
Code restructure
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The :class:`musicbrainzapi.api.lyrics.concrete_builder.LyricsConcreteBuilder` class could be improved. Many of the methods defined in here no longer need to be present. Some of the functionality (url checking for example) could be removed and implemented in other ways (a Mixin is one solution).
|
||||||
|
|
||||||
|
If other ways of filtering were to be added (as opposed to the current default of just Albums) then this class would be useful to build our :class:`musicbrainzapi.api.lyrics.Lyrics` objects consistently.
|
||||||
|
|
||||||
|
Additional functionality to the lyrics command
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
The command could be improved in a few ways:
|
||||||
|
|
||||||
|
Different aggregations
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ability for the user to specify something other than album or year to group by. For artists with large libraries, it might be useful to see results aggregated by other types of releases.
|
||||||
|
|
||||||
|
Multiple artists
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Searching for multiple artists and comparing is certainly possible in the current iteration (click provides a nice way to accept multiple artists and then we create our ``Lyrics`` objects from these) this wasn't implemented. There are rate limiting factors which may slow down the program and in the current implementation it could increase runtime considerably.
|
||||||
|
|
||||||
|
Speed improvements
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The musicbrainz api isn't too slow, however, the lyrics.ovh api can be.
|
||||||
|
|
||||||
|
One solution would be to implement threading - as we are waiting on HTTP requests this suggests threading could be a good candidate. An alternative to threading (if we are dealing with many requests) could be asyncio.
|
||||||
|
|
||||||
|
This wasn't implemented primarily because of time - but threading could be implemented on each call we make to the API.
|
||||||
|
|
||||||
|
An alternative, and I believe an interesting solution, would be to use AWS Lambda (serverless).
|
||||||
|
|
||||||
|
There is a caveat to this solution and it is cost - threading is free but adds development time and increases complexity. AWS isn't free but allows you to scale the requests out.
|
||||||
|
|
||||||
|
A solution would be to use a module like `Zappa`_. I have used this module before and it is a great tool to create lambda functions quickly.
|
||||||
|
|
||||||
|
If more control was needed one solution could be:
|
||||||
|
|
||||||
|
- Generate UUID of the current instance
|
||||||
|
- For each request to the API, dispatch a lambda function (using ``boto3``) which will run against the api. This function should take the UUID from before.
|
||||||
|
- Once finished either
|
||||||
|
|
||||||
|
+ Save results in DynamoDB with the UUID
|
||||||
|
+ Send results to SQS/SNS (not desirable, the lyrics size could be large)
|
||||||
|
|
||||||
|
- As soon as the lambdas have been dispatched, the script could either poll from a queue, or read the events queue of the DynamoDB to retrieve the results. Processing the lyrics could then begin.
|
||||||
|
|
||||||
|
This requires the user to have an internet connection - which is a current requirement. Requests to the api could be made simultaneously - without adding the complexity that comes with threading. This would not solve any API rate limiting - we are required to provide an application user_agent to the api to identify the app.
|
||||||
|
|
||||||
|
An interesting solution, and one I did consider, was to have the program run entirely in lambda, requiring no depdencies and just a simple front end that sends requests, and uses ``boto3`` to retrieve. The simplicity of this, and the fact that AWS provide an SDK for many languages, means the cient code could run in any language.
|
||||||
|
|
||||||
|
An interface to AWS API Gateway would provide the entry point to the lambda.
|
||||||
|
|
||||||
|
Writing it in this manner (with an api backend) would mean a webapp of the program could be possible, with the frontend served with something like ``Vuejs`` or ``React``.
|
||||||
|
|
||||||
|
.. _Zappa: https://github.com/Miserlou/Zappa
|
||||||
|
|
||||||
|
Error catching
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Handling missing data from both APIs is done with error catching (namely ``ValueError`` and ``TypeError``).
|
||||||
|
|
||||||
|
Although inelegant, and not guaranteed to capture the specific behaviour we want to catch (missing data etc.) it is a solution and appears to work quite well.
|
||||||
|
|
||||||
|
Musicbrainz provides a schema for their api. If this were to be placed in a production environment then readdressing this should be a priority - we should be checking the values returned, using the schema as a guide, and replacing missing values accordingly. We should not rely on ``try except`` blocks to do this as it can be unreliable and is prone to raise other errors.
|
||||||
|
|
||||||
|
Further statistical analysis
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Standard descriptive statistics are provided. I did consider including a more deeper analysis but opted not to for several reasons:
|
||||||
|
|
||||||
|
- Without a specific problem or question to answer - explorative work can take a lot of time and may not yield satisfactory results. Questions I did consider are:
|
||||||
|
|
||||||
|
+ `For active artists, based on their previous lyrics count what is the predicition of their next album?` Although a sensible question I'm not sure how useful the predicition would be - I am sure for some artists they would follow a pattern over time, but I'm not convinced all artists would and I imagine the results would be mixed.
|
||||||
|
+ `Anomaly detection - for artists with large releases, what albums stood out as larger than usual and what feature (or track) caused this anomaly?` - This would be a good question to answer and we have many tools available. As we have numeric data - clustering could be a candidate (DBSCAN or even K-MEANS). I opted not to because of time and the fact it would bloat the requirements up. Feature flags are an option when handling extra packages, ``pip install musicbrainzapi[analysis]`` for example, but nonetheless this would be an interesting question to answer and I beleive one of the easier ones to implement if it was desired.
|
||||||
100
docs/source/conf.py
Normal file
100
docs/source/conf.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# This file only contains a selection of the most common options. For a full
|
||||||
|
# list see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
# -- Path setup --------------------------------------------------------------
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
# import os
|
||||||
|
# import sys
|
||||||
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
import musicbrainzapi
|
||||||
|
from musicbrainzapi.__version__ import __version__
|
||||||
|
import sphinx_rtd_theme
|
||||||
|
import sphinx_click
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
|
project = 'musicbrainzapi'
|
||||||
|
copyright = '2020, Daniel Tomlinson'
|
||||||
|
author = 'Daniel Tomlinson'
|
||||||
|
|
||||||
|
# The full version, including alpha/beta/rc tags
|
||||||
|
release = __version__
|
||||||
|
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.viewcode',
|
||||||
|
'sphinx.ext.napoleon',
|
||||||
|
'sphinx.ext.todo',
|
||||||
|
'sphinx_click.ext',
|
||||||
|
'sphinx.ext.intersphinx',
|
||||||
|
'sphinx.ext.autosectionlabel'
|
||||||
|
]
|
||||||
|
|
||||||
|
# -- Napoleon Settings -----------------------------------------------------
|
||||||
|
napoleon_google_docstring = False
|
||||||
|
napoleon_numpy_docstring = True
|
||||||
|
napoleon_include_init_with_doc = True
|
||||||
|
napoleon_include_private_with_doc = True
|
||||||
|
napoleon_include_special_with_doc = False
|
||||||
|
napoleon_use_admonition_for_examples = False
|
||||||
|
napoleon_use_admonition_for_notes = False
|
||||||
|
napoleon_use_admonition_for_references = False
|
||||||
|
napoleon_use_ivar = True
|
||||||
|
napoleon_use_param = True
|
||||||
|
napoleon_use_rtype = True
|
||||||
|
napoleon_use_keyword = True
|
||||||
|
autodoc_member_order = 'bysource'
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = "sphinx_rtd_theme"
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
|
||||||
|
html_theme = "sphinx_rtd_theme"
|
||||||
|
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||||
|
html_static_path = ['_static']
|
||||||
|
html_context = {'css_files': ['_static/custom.css']}
|
||||||
|
html_theme_options = {
|
||||||
|
'collapse_navigation': True,
|
||||||
|
'display_version': True,
|
||||||
|
'prev_next_buttons_location': 'both',
|
||||||
|
'navigation_depth': -1,
|
||||||
|
#'navigation_depth': 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# Enable todo
|
||||||
|
todo_include_todos = True
|
||||||
5
docs/source/global.rst
Normal file
5
docs/source/global.rst
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.. role:: modname
|
||||||
|
:class: modname
|
||||||
|
|
||||||
|
.. role:: title
|
||||||
|
:class: title
|
||||||
30
docs/source/index.rst
Normal file
30
docs/source/index.rst
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
*****************
|
||||||
|
Table of Contents
|
||||||
|
*****************
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents
|
||||||
|
|
||||||
|
introduction
|
||||||
|
CLI
|
||||||
|
comments
|
||||||
|
changelog
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:caption: API
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
modules/modules
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:caption: Table of Contents
|
||||||
|
|
||||||
|
self
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
1
docs/source/introduction.rst
Normal file
1
docs/source/introduction.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.. include:: ../../README.rst
|
||||||
7
docs/source/modules/modules.rst
Normal file
7
docs/source/modules/modules.rst
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
musicbrainzapi
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
musicbrainzapi
|
||||||
38
docs/source/modules/musicbrainzapi.api.lyrics.rst
Normal file
38
docs/source/modules/musicbrainzapi.api.lyrics.rst
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
musicbrainzapi.api.lyrics package
|
||||||
|
=================================
|
||||||
|
|
||||||
|
.. automodule:: musicbrainzapi.api.lyrics
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
musicbrainzapi.api.lyrics.builder module
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. automodule:: musicbrainzapi.api.lyrics.builder
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
musicbrainzapi.api.lyrics.concrete_builder module
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. automodule:: musicbrainzapi.api.lyrics.concrete_builder
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
musicbrainzapi.api.lyrics.director module
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. automodule:: musicbrainzapi.api.lyrics.director
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:private-members:
|
||||||
28
docs/source/modules/musicbrainzapi.api.rst
Normal file
28
docs/source/modules/musicbrainzapi.api.rst
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
musicbrainzapi.api package
|
||||||
|
===========================
|
||||||
|
|
||||||
|
.. automodule:: musicbrainzapi.api
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
Subpackages
|
||||||
|
-----------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
musicbrainzapi.api.lyrics
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
musicbrainzapi.api.authenticate module
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. automodule:: musicbrainzapi.api.authenticate
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:private-members:
|
||||||
19
docs/source/modules/musicbrainzapi.rst
Normal file
19
docs/source/modules/musicbrainzapi.rst
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
musicbrainzapi
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. automodule:: musicbrainzapi
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:private-members:
|
||||||
|
|
||||||
|
|
||||||
|
Subpackages
|
||||||
|
-----------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
musicbrainzapi.api
|
||||||
|
musicbrainzapi.wordcloud
|
||||||
|
|
||||||
9
docs/source/modules/musicbrainzapi.wordcloud.rst
Normal file
9
docs/source/modules/musicbrainzapi.wordcloud.rst
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
********************************
|
||||||
|
musicbrainzapi.wordcloud package
|
||||||
|
********************************
|
||||||
|
|
||||||
|
.. automodule:: musicbrainzapi.wordcloud
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
:private-members:
|
||||||
578
poetry.lock
generated
578
poetry.lock
generated
@@ -6,6 +6,14 @@ optional = false
|
|||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "A configurable sidebar-enabled Sphinx theme"
|
||||||
|
name = "alabaster"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "0.7.12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
@@ -48,6 +56,25 @@ version = "1.5"
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pycodestyle = ">=2.5.0"
|
pycodestyle = ">=2.5.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Internationalization utilities"
|
||||||
|
name = "babel"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
version = "2.8.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytz = ">=2015.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Print ASCII tables for terminals"
|
||||||
|
name = "beautifultable"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "0.8.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "The uncompromising code formatter."
|
description = "The uncompromising code formatter."
|
||||||
@@ -85,7 +112,7 @@ python-versions = "*"
|
|||||||
version = "3.0.4"
|
version = "3.0.4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "main"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
name = "click"
|
name = "click"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -101,6 +128,36 @@ optional = false
|
|||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Code coverage measurement for Python"
|
||||||
|
name = "coverage"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||||
|
version = "5.0.3"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
toml = ["toml"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Composable style cycles"
|
||||||
|
name = "cycler"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "0.10.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
six = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Docutils -- Python Documentation Utilities"
|
||||||
|
name = "docutils"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
version = "0.16"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
@@ -109,6 +166,14 @@ optional = false
|
|||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
version = "2.9"
|
version = "2.9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Getting image size from png/jpeg/jpeg2000/gif file"
|
||||||
|
name = "imagesize"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
version = "1.2.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Read metadata from Python packages"
|
description = "Read metadata from Python packages"
|
||||||
@@ -139,6 +204,54 @@ parso = ">=0.5.2"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["colorama (0.4.1)", "docopt", "pytest (>=3.9.0,<5.0.0)"]
|
testing = ["colorama (0.4.1)", "docopt", "pytest (>=3.9.0,<5.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "A very fast and expressive template engine."
|
||||||
|
name = "jinja2"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
version = "2.11.1"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=0.23"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
i18n = ["Babel (>=0.8)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "A fast implementation of the Cassowary constraint solver"
|
||||||
|
name = "kiwisolver"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
version = "1.1.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
setuptools = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
|
name = "markupsafe"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||||
|
version = "1.1.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Python plotting package"
|
||||||
|
name = "matplotlib"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
version = "3.2.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cycler = ">=0.10"
|
||||||
|
kiwisolver = ">=1.0.1"
|
||||||
|
numpy = ">=1.11"
|
||||||
|
pyparsing = ">=2.0.1,<2.0.4 || >2.0.4,<2.1.2 || >2.1.2,<2.1.6 || >2.1.6"
|
||||||
|
python-dateutil = ">=2.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "McCabe checker, plugin for flake8"
|
description = "McCabe checker, plugin for flake8"
|
||||||
@@ -163,6 +276,14 @@ optional = false
|
|||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "NumPy is the fundamental package for array computing with Python."
|
||||||
|
name = "numpy"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
version = "1.18.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
@@ -194,6 +315,22 @@ optional = false
|
|||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Python Build Reasonableness"
|
||||||
|
name = "pbr"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "5.4.4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Python Imaging Library (Fork)"
|
||||||
|
name = "pillow"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
version = "7.0.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "plugin and hook calling mechanisms for python"
|
description = "plugin and hook calling mechanisms for python"
|
||||||
@@ -210,14 +347,6 @@ version = ">=0.12"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Easy to use progress bars"
|
|
||||||
name = "progress"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "1.5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "A full-screen, console-based Python debugger"
|
description = "A full-screen, console-based Python debugger"
|
||||||
@@ -290,7 +419,7 @@ toml = "*"
|
|||||||
dev = ["isort", "flake8", "pytest", "mypy"]
|
dev = ["isort", "flake8", "pytest", "mypy"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "main"
|
||||||
description = "Python parsing module"
|
description = "Python parsing module"
|
||||||
name = "pyparsing"
|
name = "pyparsing"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -323,6 +452,17 @@ version = ">=0.12"
|
|||||||
checkqa-mypy = ["mypy (v0.761)"]
|
checkqa-mypy = ["mypy (v0.761)"]
|
||||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Extensions to the standard Python datetime module"
|
||||||
|
name = "python-dateutil"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
version = "2.8.1"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
six = ">=1.5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "JSON RPC 2.0 server library"
|
description = "JSON RPC 2.0 server library"
|
||||||
@@ -364,6 +504,14 @@ rope = ["rope (>0.10.5)"]
|
|||||||
test = ["versioneer", "pylint", "pytest", "mock", "pytest-cov", "coverage", "numpy", "pandas", "matplotlib", "pyqt5"]
|
test = ["versioneer", "pylint", "pytest", "mock", "pytest-cov", "coverage", "numpy", "pandas", "matplotlib", "pyqt5"]
|
||||||
yapf = ["yapf"]
|
yapf = ["yapf"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "World timezone definitions, modern and historical"
|
||||||
|
name = "pytz"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "2019.3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Alternative regular expression module, to replace re."
|
description = "Alternative regular expression module, to replace re."
|
||||||
@@ -402,7 +550,7 @@ version = "0.16.0"
|
|||||||
dev = ["pytest"]
|
dev = ["pytest"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "main"
|
||||||
description = "Python 2 and 3 compatibility utilities"
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
name = "six"
|
name = "six"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -417,6 +565,131 @@ optional = false
|
|||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Python documentation generator"
|
||||||
|
name = "sphinx"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
version = "2.4.4"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Jinja2 = ">=2.3"
|
||||||
|
Pygments = ">=2.0"
|
||||||
|
alabaster = ">=0.7,<0.8"
|
||||||
|
babel = ">=1.3,<2.0 || >2.0"
|
||||||
|
colorama = ">=0.3.5"
|
||||||
|
docutils = ">=0.12"
|
||||||
|
imagesize = "*"
|
||||||
|
packaging = "*"
|
||||||
|
requests = ">=2.5.0"
|
||||||
|
setuptools = "*"
|
||||||
|
snowballstemmer = ">=1.1"
|
||||||
|
sphinxcontrib-applehelp = "*"
|
||||||
|
sphinxcontrib-devhelp = "*"
|
||||||
|
sphinxcontrib-htmlhelp = "*"
|
||||||
|
sphinxcontrib-jsmath = "*"
|
||||||
|
sphinxcontrib-qthelp = "*"
|
||||||
|
sphinxcontrib-serializinghtml = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinxcontrib-websupport"]
|
||||||
|
test = ["pytest (<5.3.3)", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Sphinx extension that automatically documents click applications"
|
||||||
|
name = "sphinx-click"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "2.3.1"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pbr = ">=2.0"
|
||||||
|
sphinx = ">=1.5,<3.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Read the Docs theme for Sphinx"
|
||||||
|
name = "sphinx-rtd-theme"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "0.4.3"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
sphinx = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
|
||||||
|
name = "sphinxcontrib-applehelp"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
version = "1.0.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||||
|
test = ["pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
|
||||||
|
name = "sphinxcontrib-devhelp"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
version = "1.0.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||||
|
test = ["pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
|
||||||
|
name = "sphinxcontrib-htmlhelp"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
version = "1.0.3"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||||
|
test = ["pytest", "html5lib"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "A sphinx extension which renders display math in HTML via JavaScript"
|
||||||
|
name = "sphinxcontrib-jsmath"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
version = "1.0.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest", "flake8", "mypy"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
|
||||||
|
name = "sphinxcontrib-qthelp"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
version = "1.0.3"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||||
|
test = ["pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
|
||||||
|
name = "sphinxcontrib-serializinghtml"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
version = "1.1.4"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||||
|
test = ["pytest"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||||
@@ -471,6 +744,19 @@ optional = false
|
|||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "A little word cloud generator"
|
||||||
|
name = "wordcloud"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "1.6.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
matplotlib = "*"
|
||||||
|
numpy = ">=1.6.1"
|
||||||
|
pillow = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "A formatter for Python code."
|
description = "A formatter for Python code."
|
||||||
@@ -493,7 +779,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
|||||||
testing = ["jaraco.itertools", "func-timeout"]
|
testing = ["jaraco.itertools", "func-timeout"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "50cff0758d9f4bfa77596b28f6f5f0c7f1db897b7c0d615071379c288b1d110d"
|
content-hash = "78a3288551032af1e115b4920ee345cb1a4fcbfcca3c7caca6bd6f7935ac3876"
|
||||||
python-versions = "^3.7"
|
python-versions = "^3.7"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
@@ -501,6 +787,10 @@ addict = [
|
|||||||
{file = "addict-2.2.1-py3-none-any.whl", hash = "sha256:1948c2a5d93ba6026eb91aef2c971234aaf72488a9c07ab8a7950f82ae30eea7"},
|
{file = "addict-2.2.1-py3-none-any.whl", hash = "sha256:1948c2a5d93ba6026eb91aef2c971234aaf72488a9c07ab8a7950f82ae30eea7"},
|
||||||
{file = "addict-2.2.1.tar.gz", hash = "sha256:f22493f056032f50e4931a82444fcba8ef74c8fc994c5d06aa546a1433c2b8b0"},
|
{file = "addict-2.2.1.tar.gz", hash = "sha256:f22493f056032f50e4931a82444fcba8ef74c8fc994c5d06aa546a1433c2b8b0"},
|
||||||
]
|
]
|
||||||
|
alabaster = [
|
||||||
|
{file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
|
||||||
|
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
|
||||||
|
]
|
||||||
appdirs = [
|
appdirs = [
|
||||||
{file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
|
{file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
|
||||||
{file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"},
|
{file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"},
|
||||||
@@ -516,6 +806,14 @@ attrs = [
|
|||||||
autopep8 = [
|
autopep8 = [
|
||||||
{file = "autopep8-1.5.tar.gz", hash = "sha256:0f592a0447acea0c2b0a9602be1e4e3d86db52badd2e3c84f0193bfd89fd3a43"},
|
{file = "autopep8-1.5.tar.gz", hash = "sha256:0f592a0447acea0c2b0a9602be1e4e3d86db52badd2e3c84f0193bfd89fd3a43"},
|
||||||
]
|
]
|
||||||
|
babel = [
|
||||||
|
{file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"},
|
||||||
|
{file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"},
|
||||||
|
]
|
||||||
|
beautifultable = [
|
||||||
|
{file = "beautifultable-0.8.0-py2.py3-none-any.whl", hash = "sha256:28e2e93d44a4e84511c4869da4b907345435a06728925e295790f24e1d57300c"},
|
||||||
|
{file = "beautifultable-0.8.0.tar.gz", hash = "sha256:d44d9551bbed7bfa88675324f84efb9aa857384d44e9fb21eb530f0a0badb815"},
|
||||||
|
]
|
||||||
black = [
|
black = [
|
||||||
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
||||||
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
||||||
@@ -536,10 +834,55 @@ colorama = [
|
|||||||
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
|
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
|
||||||
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
||||||
]
|
]
|
||||||
|
coverage = [
|
||||||
|
{file = "coverage-5.0.3-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f"},
|
||||||
|
{file = "coverage-5.0.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc"},
|
||||||
|
{file = "coverage-5.0.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a"},
|
||||||
|
{file = "coverage-5.0.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52"},
|
||||||
|
{file = "coverage-5.0.3-cp27-cp27m-win32.whl", hash = "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c"},
|
||||||
|
{file = "coverage-5.0.3-cp27-cp27m-win_amd64.whl", hash = "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73"},
|
||||||
|
{file = "coverage-5.0.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68"},
|
||||||
|
{file = "coverage-5.0.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691"},
|
||||||
|
{file = "coverage-5.0.3-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301"},
|
||||||
|
{file = "coverage-5.0.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf"},
|
||||||
|
{file = "coverage-5.0.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3"},
|
||||||
|
{file = "coverage-5.0.3-cp35-cp35m-win32.whl", hash = "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"},
|
||||||
|
{file = "coverage-5.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0"},
|
||||||
|
{file = "coverage-5.0.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2"},
|
||||||
|
{file = "coverage-5.0.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894"},
|
||||||
|
{file = "coverage-5.0.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf"},
|
||||||
|
{file = "coverage-5.0.3-cp36-cp36m-win32.whl", hash = "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477"},
|
||||||
|
{file = "coverage-5.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc"},
|
||||||
|
{file = "coverage-5.0.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8"},
|
||||||
|
{file = "coverage-5.0.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987"},
|
||||||
|
{file = "coverage-5.0.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea"},
|
||||||
|
{file = "coverage-5.0.3-cp37-cp37m-win32.whl", hash = "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc"},
|
||||||
|
{file = "coverage-5.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e"},
|
||||||
|
{file = "coverage-5.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb"},
|
||||||
|
{file = "coverage-5.0.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37"},
|
||||||
|
{file = "coverage-5.0.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d"},
|
||||||
|
{file = "coverage-5.0.3-cp38-cp38m-win32.whl", hash = "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954"},
|
||||||
|
{file = "coverage-5.0.3-cp38-cp38m-win_amd64.whl", hash = "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e"},
|
||||||
|
{file = "coverage-5.0.3-cp39-cp39m-win32.whl", hash = "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40"},
|
||||||
|
{file = "coverage-5.0.3-cp39-cp39m-win_amd64.whl", hash = "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af"},
|
||||||
|
{file = "coverage-5.0.3.tar.gz", hash = "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef"},
|
||||||
|
]
|
||||||
|
cycler = [
|
||||||
|
{file = "cycler-0.10.0-py2.py3-none-any.whl", hash = "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d"},
|
||||||
|
{file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"},
|
||||||
|
]
|
||||||
|
docutils = [
|
||||||
|
{file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"},
|
||||||
|
{file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"},
|
||||||
|
]
|
||||||
idna = [
|
idna = [
|
||||||
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
|
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
|
||||||
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
|
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
|
||||||
]
|
]
|
||||||
|
imagesize = [
|
||||||
|
{file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
|
||||||
|
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
|
||||||
|
]
|
||||||
importlib-metadata = [
|
importlib-metadata = [
|
||||||
{file = "importlib_metadata-1.5.0-py2.py3-none-any.whl", hash = "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"},
|
{file = "importlib_metadata-1.5.0-py2.py3-none-any.whl", hash = "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"},
|
||||||
{file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"},
|
{file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"},
|
||||||
@@ -548,6 +891,100 @@ jedi = [
|
|||||||
{file = "jedi-0.15.2-py2.py3-none-any.whl", hash = "sha256:1349c1e8c107095a55386628bb3b2a79422f3a2cab8381e34ce19909e0cf5064"},
|
{file = "jedi-0.15.2-py2.py3-none-any.whl", hash = "sha256:1349c1e8c107095a55386628bb3b2a79422f3a2cab8381e34ce19909e0cf5064"},
|
||||||
{file = "jedi-0.15.2.tar.gz", hash = "sha256:e909527104a903606dd63bea6e8e888833f0ef087057829b89a18364a856f807"},
|
{file = "jedi-0.15.2.tar.gz", hash = "sha256:e909527104a903606dd63bea6e8e888833f0ef087057829b89a18364a856f807"},
|
||||||
]
|
]
|
||||||
|
jinja2 = [
|
||||||
|
{file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
|
||||||
|
{file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"},
|
||||||
|
]
|
||||||
|
kiwisolver = [
|
||||||
|
{file = "kiwisolver-1.1.0-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7f4dd50874177d2bb060d74769210f3bce1af87a8c7cf5b37d032ebf94f0aca3"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:fe51b79da0062f8e9d49ed0182a626a7dc7a0cbca0328f612c6ee5e4711c81e4"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f790f8b3dff3d53453de6a7b7ddd173d2e020fb160baff578d578065b108a05f"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b22153870ca5cf2ab9c940d7bc38e8e9089fa0f7e5856ea195e1cf4ff43d5a"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e8bf074363ce2babeb4764d94f8e65efd22e6a7c74860a4f05a6947afc020ff2"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp27-none-win32.whl", hash = "sha256:47b8cb81a7d18dbaf4fed6a61c3cecdb5adec7b4ac292bddb0d016d57e8507d5"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp27-none-win_amd64.whl", hash = "sha256:b64916959e4ae0ac78af7c3e8cef4becee0c0e9694ad477b4c6b3a536de6a544"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp34-cp34m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:682e54f0ce8f45981878756d7203fd01e188cc6c8b2c5e2cf03675390b4534d5"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:d52e3b1868a4e8fd18b5cb15055c76820df514e26aa84cc02f593d99fef6707f"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:8aa7009437640beb2768bfd06da049bad0df85f47ff18426261acecd1cf00897"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp34-none-win32.whl", hash = "sha256:26f4fbd6f5e1dabff70a9ba0d2c4bd30761086454aa30dddc5b52764ee4852b7"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp34-none-win_amd64.whl", hash = "sha256:79bfb2f0bd7cbf9ea256612c9523367e5ec51d7cd616ae20ca2c90f575d839a2"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:3b2378ad387f49cbb328205bda569b9f87288d6bc1bf4cd683c34523a2341efe"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:aa716b9122307c50686356cfb47bfbc66541868078d0c801341df31dca1232a9"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:58e626e1f7dfbb620d08d457325a4cdac65d1809680009f46bf41eaf74ad0187"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:e3a21a720791712ed721c7b95d433e036134de6f18c77dbe96119eaf7aa08004"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp35-none-win32.whl", hash = "sha256:939f36f21a8c571686eb491acfffa9c7f1ac345087281b412d63ea39ca14ec4a"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp35-none-win_amd64.whl", hash = "sha256:9733b7f64bd9f807832d673355f79703f81f0b3e52bfce420fc00d8cb28c6a6c"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:acc4df99308111585121db217681f1ce0eecb48d3a828a2f9bbf9773f4937e9e"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:9105ce82dcc32c73eb53a04c869b6a4bc756b43e4385f76ea7943e827f529e4d"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f16814a4a96dc04bf1da7d53ee8d5b1d6decfc1a92a63349bb15d37b6a263dd9"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:400599c0fe58d21522cae0e8b22318e09d9729451b17ee61ba8e1e7c0346565c"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp36-none-win32.whl", hash = "sha256:db1a5d3cc4ae943d674718d6c47d2d82488ddd94b93b9e12d24aabdbfe48caee"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp36-none-win_amd64.whl", hash = "sha256:5a52e1b006bfa5be04fe4debbcdd2688432a9af4b207a3f429c74ad625022641"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:a02f6c3e229d0b7220bd74600e9351e18bc0c361b05f29adae0d10599ae0e326"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9491578147849b93e70d7c1d23cb1229458f71fc79c51d52dce0809b2ca44eea"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5c7ca4e449ac9f99b3b9d4693debb1d6d237d1542dd6a56b3305fe8a9620f883"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a0c0a9f06872330d0dd31b45607197caab3c22777600e88031bfe66799e70bb0"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp37-none-win32.whl", hash = "sha256:8944a16020c07b682df861207b7e0efcd2f46c7488619cb55f65882279119389"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp37-none-win_amd64.whl", hash = "sha256:d3fcf0819dc3fea58be1fd1ca390851bdb719a549850e708ed858503ff25d995"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:933df612c453928f1c6faa9236161a1d999a26cd40abf1dc5d7ebbc6dbfb8fca"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d22702cadb86b6fcba0e6b907d9f84a312db9cd6934ee728144ce3018e715ee1"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:210d8c39d01758d76c2b9a693567e1657ec661229bc32eac30761fa79b2474b0"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp38-none-win32.whl", hash = "sha256:76275ee077772c8dde04fb6c5bc24b91af1bb3e7f4816fd1852f1495a64dad93"},
|
||||||
|
{file = "kiwisolver-1.1.0-cp38-none-win_amd64.whl", hash = "sha256:3b15d56a9cd40c52d7ab763ff0bc700edbb4e1a298dc43715ecccd605002cf11"},
|
||||||
|
{file = "kiwisolver-1.1.0.tar.gz", hash = "sha256:53eaed412477c836e1b9522c19858a8557d6e595077830146182225613b11a75"},
|
||||||
|
]
|
||||||
|
markupsafe = [
|
||||||
|
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
|
||||||
|
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
|
||||||
|
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
|
||||||
|
]
|
||||||
|
matplotlib = [
|
||||||
|
{file = "matplotlib-3.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0711b07920919951b2c508a773c433cbe07bdad952ea84ed9d18ca7853ccbe8b"},
|
||||||
|
{file = "matplotlib-3.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b93377c6720e7db9cbba57e856a21aae2ff707677a6ee6b3b9d485f22ed82697"},
|
||||||
|
{file = "matplotlib-3.2.0-cp36-cp36m-win32.whl", hash = "sha256:8e931015769322ee6860cabb8f975f628788e851092fd5edbdb065b5a516e3af"},
|
||||||
|
{file = "matplotlib-3.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b21479a4478070c1c0f460e1bf1b65341e6a70ae0da905fcee836651450c66bb"},
|
||||||
|
{file = "matplotlib-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab307e610302971012dc2387c97fc68e58c8eb00045a2c735da1b16353a3e3f"},
|
||||||
|
{file = "matplotlib-3.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d75f5e952562f5e494ae92c1f917fc96c2ce09305a7c1bdc2e6502d3c61fbdc3"},
|
||||||
|
{file = "matplotlib-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:9d174cc9681184023a7d520079eb0c085208761c6562710c1de7263d08217ab6"},
|
||||||
|
{file = "matplotlib-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d281862a68b0bfce8f9e02a8e5acaa5cfbec37f37320f59b52eaf54b6423ec13"},
|
||||||
|
{file = "matplotlib-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee8acb1d4ee204e5cfe361d8f00d7e52c68f81c099b6c6048a3c76bf2c6b46e6"},
|
||||||
|
{file = "matplotlib-3.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:be937f34047bc09ed22d6a19d970fdc61d5d3191aa62f3262fc7f308e6d2e7f9"},
|
||||||
|
{file = "matplotlib-3.2.0-cp38-cp38-win32.whl", hash = "sha256:97a03e73f9ab71db8e4084894550c3af420c8ab1989b5e1306261b17576bf61b"},
|
||||||
|
{file = "matplotlib-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5287cfcabad6f0f71a2627c1bbb6fb0cddacb9844f6c91f210604faa508f562"},
|
||||||
|
{file = "matplotlib-3.2.0-pp373-pypy36_pp73-win32.whl", hash = "sha256:fc84f7c7cf1c5a9dbceadb7546818228f019d3b113ce5e362120c895fbba2944"},
|
||||||
|
{file = "matplotlib-3.2.0.tar.gz", hash = "sha256:651d76daf9168250370d4befb09f79875daa2224a9096d97dfc3ed764c842be4"},
|
||||||
|
]
|
||||||
mccabe = [
|
mccabe = [
|
||||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||||
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
||||||
@@ -560,6 +997,29 @@ musicbrainzngs = [
|
|||||||
{file = "musicbrainzngs-0.7.1-py2.py3-none-any.whl", hash = "sha256:e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10"},
|
{file = "musicbrainzngs-0.7.1-py2.py3-none-any.whl", hash = "sha256:e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10"},
|
||||||
{file = "musicbrainzngs-0.7.1.tar.gz", hash = "sha256:ab1c0100fd0b305852e65f2ed4113c6de12e68afd55186987b8ed97e0f98e627"},
|
{file = "musicbrainzngs-0.7.1.tar.gz", hash = "sha256:ab1c0100fd0b305852e65f2ed4113c6de12e68afd55186987b8ed97e0f98e627"},
|
||||||
]
|
]
|
||||||
|
numpy = [
|
||||||
|
{file = "numpy-1.18.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:20b26aaa5b3da029942cdcce719b363dbe58696ad182aff0e5dcb1687ec946dc"},
|
||||||
|
{file = "numpy-1.18.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:70a840a26f4e61defa7bdf811d7498a284ced303dfbc35acb7be12a39b2aa121"},
|
||||||
|
{file = "numpy-1.18.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:17aa7a81fe7599a10f2b7d95856dc5cf84a4eefa45bc96123cbbc3ebc568994e"},
|
||||||
|
{file = "numpy-1.18.1-cp35-cp35m-win32.whl", hash = "sha256:f3d0a94ad151870978fb93538e95411c83899c9dc63e6fb65542f769568ecfa5"},
|
||||||
|
{file = "numpy-1.18.1-cp35-cp35m-win_amd64.whl", hash = "sha256:1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6"},
|
||||||
|
{file = "numpy-1.18.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ae0975f42ab1f28364dcda3dde3cf6c1ddab3e1d4b2909da0cb0191fa9ca0480"},
|
||||||
|
{file = "numpy-1.18.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cf7eb6b1025d3e169989416b1adcd676624c2dbed9e3bcb7137f51bfc8cc2572"},
|
||||||
|
{file = "numpy-1.18.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b765ed3930b92812aa698a455847141869ef755a87e099fddd4ccf9d81fffb57"},
|
||||||
|
{file = "numpy-1.18.1-cp36-cp36m-win32.whl", hash = "sha256:2d75908ab3ced4223ccba595b48e538afa5ecc37405923d1fea6906d7c3a50bc"},
|
||||||
|
{file = "numpy-1.18.1-cp36-cp36m-win_amd64.whl", hash = "sha256:9acdf933c1fd263c513a2df3dceecea6f3ff4419d80bf238510976bf9bcb26cd"},
|
||||||
|
{file = "numpy-1.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56bc8ded6fcd9adea90f65377438f9fea8c05fcf7c5ba766bef258d0da1554aa"},
|
||||||
|
{file = "numpy-1.18.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e422c3152921cece8b6a2fb6b0b4d73b6579bd20ae075e7d15143e711f3ca2ca"},
|
||||||
|
{file = "numpy-1.18.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b3af02ecc999c8003e538e60c89a2b37646b39b688d4e44d7373e11c2debabec"},
|
||||||
|
{file = "numpy-1.18.1-cp37-cp37m-win32.whl", hash = "sha256:d92350c22b150c1cae7ebb0ee8b5670cc84848f6359cf6b5d8f86617098a9b73"},
|
||||||
|
{file = "numpy-1.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:77c3bfe65d8560487052ad55c6998a04b654c2fbc36d546aef2b2e511e760971"},
|
||||||
|
{file = "numpy-1.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c98c5ffd7d41611407a1103ae11c8b634ad6a43606eca3e2a5a269e5d6e8eb07"},
|
||||||
|
{file = "numpy-1.18.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9537eecf179f566fd1c160a2e912ca0b8e02d773af0a7a1120ad4f7507cd0d26"},
|
||||||
|
{file = "numpy-1.18.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e840f552a509e3380b0f0ec977e8124d0dc34dc0e68289ca28f4d7c1d0d79474"},
|
||||||
|
{file = "numpy-1.18.1-cp38-cp38-win32.whl", hash = "sha256:590355aeade1a2eaba17617c19edccb7db8d78760175256e3cf94590a1a964f3"},
|
||||||
|
{file = "numpy-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:39d2c685af15d3ce682c99ce5925cc66efc824652e10990d2462dfe9b8918c6a"},
|
||||||
|
{file = "numpy-1.18.1.zip", hash = "sha256:b6ff59cee96b454516e47e7721098e6ceebef435e3e21ac2d6c3b8b02628eb77"},
|
||||||
|
]
|
||||||
packaging = [
|
packaging = [
|
||||||
{file = "packaging-20.1-py2.py3-none-any.whl", hash = "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73"},
|
{file = "packaging-20.1-py2.py3-none-any.whl", hash = "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73"},
|
||||||
{file = "packaging-20.1.tar.gz", hash = "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"},
|
{file = "packaging-20.1.tar.gz", hash = "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"},
|
||||||
@@ -572,13 +1032,38 @@ pathspec = [
|
|||||||
{file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"},
|
{file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"},
|
||||||
{file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"},
|
{file = "pathspec-0.7.0.tar.gz", hash = "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"},
|
||||||
]
|
]
|
||||||
|
pbr = [
|
||||||
|
{file = "pbr-5.4.4-py2.py3-none-any.whl", hash = "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"},
|
||||||
|
{file = "pbr-5.4.4.tar.gz", hash = "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b"},
|
||||||
|
]
|
||||||
|
pillow = [
|
||||||
|
{file = "Pillow-7.0.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:5f3546ceb08089cedb9e8ff7e3f6a7042bb5b37c2a95d392fb027c3e53a2da00"},
|
||||||
|
{file = "Pillow-7.0.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:9d2ba4ed13af381233e2d810ff3bab84ef9f18430a9b336ab69eaf3cd24299ff"},
|
||||||
|
{file = "Pillow-7.0.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ff3797f2f16bf9d17d53257612da84dd0758db33935777149b3334c01ff68865"},
|
||||||
|
{file = "Pillow-7.0.0-cp35-cp35m-win32.whl", hash = "sha256:c18f70dc27cc5d236f10e7834236aff60aadc71346a5bc1f4f83a4b3abee6386"},
|
||||||
|
{file = "Pillow-7.0.0-cp35-cp35m-win_amd64.whl", hash = "sha256:875358310ed7abd5320f21dd97351d62de4929b0426cdb1eaa904b64ac36b435"},
|
||||||
|
{file = "Pillow-7.0.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:ab76e5580b0ed647a8d8d2d2daee170e8e9f8aad225ede314f684e297e3643c2"},
|
||||||
|
{file = "Pillow-7.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a62ec5e13e227399be73303ff301f2865bf68657d15ea50b038d25fc41097317"},
|
||||||
|
{file = "Pillow-7.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8ac6ce7ff3892e5deaab7abaec763538ffd011f74dc1801d93d3c5fc541feee2"},
|
||||||
|
{file = "Pillow-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:91b710e3353aea6fc758cdb7136d9bbdcb26b53cefe43e2cba953ac3ee1d3313"},
|
||||||
|
{file = "Pillow-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:bf598d2e37cf8edb1a2f26ed3fb255191f5232badea4003c16301cb94ac5bdd0"},
|
||||||
|
{file = "Pillow-7.0.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f"},
|
||||||
|
{file = "Pillow-7.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:dc058b7833184970d1248135b8b0ab702e6daa833be14035179f2acb78ff5636"},
|
||||||
|
{file = "Pillow-7.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9"},
|
||||||
|
{file = "Pillow-7.0.0-cp37-cp37m-win32.whl", hash = "sha256:54ebae163e8412aff0b9df1e88adab65788f5f5b58e625dc5c7f51eaf14a6837"},
|
||||||
|
{file = "Pillow-7.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:87269cc6ce1e3dee11f23fa515e4249ae678dbbe2704598a51cee76c52e19cda"},
|
||||||
|
{file = "Pillow-7.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be"},
|
||||||
|
{file = "Pillow-7.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:62a889aeb0a79e50ecf5af272e9e3c164148f4bd9636cc6bcfa182a52c8b0533"},
|
||||||
|
{file = "Pillow-7.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bf4003aa538af3f4205c5fac56eacaa67a6dd81e454ffd9e9f055fff9f1bc614"},
|
||||||
|
{file = "Pillow-7.0.0-cp38-cp38-win32.whl", hash = "sha256:7406f5a9b2fd966e79e6abdaf700585a4522e98d6559ce37fc52e5c955fade0a"},
|
||||||
|
{file = "Pillow-7.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:5f7ae9126d16194f114435ebb79cc536b5682002a4fa57fa7bb2cbcde65f2f4d"},
|
||||||
|
{file = "Pillow-7.0.0-pp373-pypy36_pp73-win32.whl", hash = "sha256:8453f914f4e5a3d828281a6628cf517832abfa13ff50679a4848926dac7c0358"},
|
||||||
|
{file = "Pillow-7.0.0.tar.gz", hash = "sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946"},
|
||||||
|
]
|
||||||
pluggy = [
|
pluggy = [
|
||||||
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
|
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
|
||||||
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
||||||
]
|
]
|
||||||
progress = [
|
|
||||||
{file = "progress-1.5.tar.gz", hash = "sha256:69ecedd1d1bbe71bf6313d88d1e6c4d2957b7f1d4f71312c211257f7dae64372"},
|
|
||||||
]
|
|
||||||
pudb = [
|
pudb = [
|
||||||
{file = "pudb-2019.2.tar.gz", hash = "sha256:e8f0ea01b134d802872184b05bffc82af29a1eb2f9374a277434b932d68f58dc"},
|
{file = "pudb-2019.2.tar.gz", hash = "sha256:e8f0ea01b134d802872184b05bffc82af29a1eb2f9374a277434b932d68f58dc"},
|
||||||
]
|
]
|
||||||
@@ -614,6 +1099,10 @@ pytest = [
|
|||||||
{file = "pytest-5.3.5-py3-none-any.whl", hash = "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6"},
|
{file = "pytest-5.3.5-py3-none-any.whl", hash = "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6"},
|
||||||
{file = "pytest-5.3.5.tar.gz", hash = "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d"},
|
{file = "pytest-5.3.5.tar.gz", hash = "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d"},
|
||||||
]
|
]
|
||||||
|
python-dateutil = [
|
||||||
|
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
|
||||||
|
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
|
||||||
|
]
|
||||||
python-jsonrpc-server = [
|
python-jsonrpc-server = [
|
||||||
{file = "python-jsonrpc-server-0.3.4.tar.gz", hash = "sha256:c73bf5495c9dd4d2f902755bedeb6da5afe778e0beee82f0e195c4655352fe37"},
|
{file = "python-jsonrpc-server-0.3.4.tar.gz", hash = "sha256:c73bf5495c9dd4d2f902755bedeb6da5afe778e0beee82f0e195c4655352fe37"},
|
||||||
{file = "python_jsonrpc_server-0.3.4-py3-none-any.whl", hash = "sha256:1f85f75f37f923149cc0aa078474b6df55b708e82ed819ca8846a65d7d0ada7f"},
|
{file = "python_jsonrpc_server-0.3.4-py3-none-any.whl", hash = "sha256:1f85f75f37f923149cc0aa078474b6df55b708e82ed819ca8846a65d7d0ada7f"},
|
||||||
@@ -622,6 +1111,10 @@ python-language-server = [
|
|||||||
{file = "python-language-server-0.31.8.tar.gz", hash = "sha256:f5685e1a6a3f6a2529ff75ea0676c59e769024302b2434564a5e7005d056eb82"},
|
{file = "python-language-server-0.31.8.tar.gz", hash = "sha256:f5685e1a6a3f6a2529ff75ea0676c59e769024302b2434564a5e7005d056eb82"},
|
||||||
{file = "python_language_server-0.31.8-py3-none-any.whl", hash = "sha256:c95470de6da223cdad7e60121bf5d220c292146caf2712eaef47a515c879e29d"},
|
{file = "python_language_server-0.31.8-py3-none-any.whl", hash = "sha256:c95470de6da223cdad7e60121bf5d220c292146caf2712eaef47a515c879e29d"},
|
||||||
]
|
]
|
||||||
|
pytz = [
|
||||||
|
{file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
|
||||||
|
{file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"},
|
||||||
|
]
|
||||||
regex = [
|
regex = [
|
||||||
{file = "regex-2020.2.20-cp27-cp27m-win32.whl", hash = "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb"},
|
{file = "regex-2020.2.20-cp27-cp27m-win32.whl", hash = "sha256:99272d6b6a68c7ae4391908fc15f6b8c9a6c345a46b632d7fdb7ef6c883a2bbb"},
|
||||||
{file = "regex-2020.2.20-cp27-cp27m-win_amd64.whl", hash = "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74"},
|
{file = "regex-2020.2.20-cp27-cp27m-win_amd64.whl", hash = "sha256:974535648f31c2b712a6b2595969f8ab370834080e00ab24e5dbb9d19b8bfb74"},
|
||||||
@@ -662,6 +1155,42 @@ snowballstemmer = [
|
|||||||
{file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"},
|
{file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"},
|
||||||
{file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
|
{file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
|
||||||
]
|
]
|
||||||
|
sphinx = [
|
||||||
|
{file = "Sphinx-2.4.4-py3-none-any.whl", hash = "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"},
|
||||||
|
{file = "Sphinx-2.4.4.tar.gz", hash = "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66"},
|
||||||
|
]
|
||||||
|
sphinx-click = [
|
||||||
|
{file = "sphinx-click-2.3.1.tar.gz", hash = "sha256:793c68b41c4a9435f953e2a27f9bf5883729037b7431f32b2776257c2966bd1b"},
|
||||||
|
{file = "sphinx_click-2.3.1-py2.py3-none-any.whl", hash = "sha256:8c6274666730686a65efbae0b4465879b030372333de3114aeb63c44204da32e"},
|
||||||
|
]
|
||||||
|
sphinx-rtd-theme = [
|
||||||
|
{file = "sphinx_rtd_theme-0.4.3-py2.py3-none-any.whl", hash = "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4"},
|
||||||
|
{file = "sphinx_rtd_theme-0.4.3.tar.gz", hash = "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"},
|
||||||
|
]
|
||||||
|
sphinxcontrib-applehelp = [
|
||||||
|
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
|
||||||
|
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
|
||||||
|
]
|
||||||
|
sphinxcontrib-devhelp = [
|
||||||
|
{file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
|
||||||
|
{file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
|
||||||
|
]
|
||||||
|
sphinxcontrib-htmlhelp = [
|
||||||
|
{file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"},
|
||||||
|
{file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"},
|
||||||
|
]
|
||||||
|
sphinxcontrib-jsmath = [
|
||||||
|
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
|
||||||
|
{file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
|
||||||
|
]
|
||||||
|
sphinxcontrib-qthelp = [
|
||||||
|
{file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
|
||||||
|
{file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
|
||||||
|
]
|
||||||
|
sphinxcontrib-serializinghtml = [
|
||||||
|
{file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"},
|
||||||
|
{file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"},
|
||||||
|
]
|
||||||
toml = [
|
toml = [
|
||||||
{file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
|
{file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
|
||||||
{file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
|
{file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
|
||||||
@@ -704,6 +1233,25 @@ wcwidth = [
|
|||||||
{file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"},
|
{file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"},
|
||||||
{file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"},
|
{file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"},
|
||||||
]
|
]
|
||||||
|
wordcloud = [
|
||||||
|
{file = "wordcloud-1.6.0-cp27-cp27m-macosx_10_6_x86_64.whl", hash = "sha256:b99157f068826697d93d2e5e61b1acff35591d5e534818368ccd56945b9a5f29"},
|
||||||
|
{file = "wordcloud-1.6.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:60c9178ea11d6537f19dad7eb5387f2516737796827710c9409ab9602d9493c7"},
|
||||||
|
{file = "wordcloud-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0baf47567bd426bf65963d53a1aaa69af35c2e096dc0ad9073efd5833cccd20a"},
|
||||||
|
{file = "wordcloud-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:52d0772e385e38144be2bdb58a0d7817f2c80db0640e1efad699cff8ea86533d"},
|
||||||
|
{file = "wordcloud-1.6.0-cp34-cp34m-macosx_10_6_x86_64.whl", hash = "sha256:61156874a21fffb46cdfb3518bbc9865fbfe9973ecc36eff20e86792687e439b"},
|
||||||
|
{file = "wordcloud-1.6.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:358f4ead931bc8297de3dbd3a26ce8d1e3fe27c1027cce091c1b7037e4ba4904"},
|
||||||
|
{file = "wordcloud-1.6.0-cp34-cp34m-win_amd64.whl", hash = "sha256:473b660baee64578dad272a18253b59245a337f5dfa3a186e32cf20b0eee4110"},
|
||||||
|
{file = "wordcloud-1.6.0-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:950882b89298c318e5f7cf10027f00b4e09402e18f719cb656aea5209a57e5a9"},
|
||||||
|
{file = "wordcloud-1.6.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a8d829e19431709c1310a505687fc7c0f869c48259f4a55b5bf387642ed6da46"},
|
||||||
|
{file = "wordcloud-1.6.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e9ae81e8dbb5953f8cf94083b990c760b179b4000dae2babd14827d61230fc69"},
|
||||||
|
{file = "wordcloud-1.6.0-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:ae6c0030a7fd09bd35713592ba005da9457f7d38f46dc807484c5e0a379d813c"},
|
||||||
|
{file = "wordcloud-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c59387b35af772626d4a87b986eb8ab29d3d7ffca6f94da95f4c3a0961407df3"},
|
||||||
|
{file = "wordcloud-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b0256ca213eb52e5261307e64faaf242742ada1322bb9d5090ecdaa9b44540ee"},
|
||||||
|
{file = "wordcloud-1.6.0-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:fc3db0cc71e4d5666f732c5b4b3c04a0d58242579cb6c6e5146ffd2890cc5d57"},
|
||||||
|
{file = "wordcloud-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d4b970d4d30bc9baec9e8b2d7e69fb9771576bb09d6b6f6ce6f22403ca58d6de"},
|
||||||
|
{file = "wordcloud-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3971ca6042745169e9645b3bbce64b790f8c211ad7c7d265049992506e033212"},
|
||||||
|
{file = "wordcloud-1.6.0.tar.gz", hash = "sha256:4335deb87b7cd9f8a6ce12de0257d15f14f98874f326e7a839f27b2c8ac792ca"},
|
||||||
|
]
|
||||||
yapf = [
|
yapf = [
|
||||||
{file = "yapf-0.29.0-py2.py3-none-any.whl", hash = "sha256:cad8a272c6001b3401de3278238fdc54997b6c2e56baa751788915f879a52fca"},
|
{file = "yapf-0.29.0-py2.py3-none-any.whl", hash = "sha256:cad8a272c6001b3401de3278238fdc54997b6c2e56baa751788915f879a52fca"},
|
||||||
{file = "yapf-0.29.0.tar.gz", hash = "sha256:712e23c468506bf12cadd10169f852572ecc61b266258422d45aaf4ad7ef43de"},
|
{file = "yapf-0.29.0.tar.gz", hash = "sha256:712e23c468506bf12cadd10169f852572ecc61b266258422d45aaf4ad7ef43de"},
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "musicbrainzapi"
|
name = "musicbrainzapi"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
description = ""
|
description = "Python module to calculate statistics and generate a wordcloud for a given artist. Uses the Musicbrainz API and the lyrics.ovh API."
|
||||||
authors = ["dtomlinson <dtomlinson@williamhill.co.uk>"]
|
license = "MIT"
|
||||||
|
authors = ["dtomlinson <dtomlinson@panaetius.co.uk>"]
|
||||||
|
readme = "/Users/dtomlinson/git-repos/cv/musicbrainzapi/README.rst"
|
||||||
|
homepage = "https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic"
|
||||||
|
repository = "https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.7"
|
python = "^3.7"
|
||||||
requests = "^2.23.0"
|
requests = "^2.23.0"
|
||||||
musicbrainzngs = "^0.7.1"
|
musicbrainzngs = "^0.7.1"
|
||||||
addict = "^2.2.1"
|
addict = "^2.2.1"
|
||||||
progress = "^1.5"
|
numpy = "^1.18.1"
|
||||||
|
beautifultable = "^0.8.0"
|
||||||
|
wordcloud = "^1.6.0"
|
||||||
|
click = "^7.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^5.2"
|
pytest = "^5.2"
|
||||||
@@ -23,10 +30,15 @@ autopep8 = "^1.5"
|
|||||||
YAPF = "^0.29.0"
|
YAPF = "^0.29.0"
|
||||||
pudb = "^2019.2"
|
pudb = "^2019.2"
|
||||||
pyls-black = "^0.4.4"
|
pyls-black = "^0.4.4"
|
||||||
|
sphinx = "^2.4.4"
|
||||||
|
sphinx_rtd_theme = "^0.4.3"
|
||||||
|
sphinx-click = "^2.3.1"
|
||||||
|
coverage = "^5.0.3"
|
||||||
|
|
||||||
|
[tool.poetry.plugins."console_scripts"]
|
||||||
|
"musicbrainzapi" = "musicbrainzapi.cli.cli:cli"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry>=0.12"]
|
||||||
build-backend = "poetry.masonry.api"
|
build-backend = "poetry.masonry.api"
|
||||||
|
|
||||||
[tool.poetry.plugins."console_scripts"]
|
|
||||||
"musicbrainzapi" = "musicbrainzapi.cli.cli:cli"
|
|
||||||
|
|||||||
71
requirements-dev.txt
Normal file
71
requirements-dev.txt
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
addict==2.2.1
|
||||||
|
alabaster==0.7.12
|
||||||
|
appdirs==1.4.3
|
||||||
|
atomicwrites==1.3.0; sys_platform == "win32"
|
||||||
|
attrs==19.3.0
|
||||||
|
autopep8==1.5
|
||||||
|
babel==2.8.0
|
||||||
|
beautifultable==0.8.0
|
||||||
|
black==19.10b0
|
||||||
|
certifi==2019.11.28
|
||||||
|
chardet==3.0.4
|
||||||
|
click==7.0
|
||||||
|
colorama==0.4.3; sys_platform == "win32"
|
||||||
|
cycler==0.10.0
|
||||||
|
docutils==0.16
|
||||||
|
idna==2.9
|
||||||
|
imagesize==1.2.0
|
||||||
|
importlib-metadata==1.5.0; python_version < "3.8"
|
||||||
|
jedi==0.15.2
|
||||||
|
jinja2==2.11.1
|
||||||
|
kiwisolver==1.1.0
|
||||||
|
markupsafe==1.1.1
|
||||||
|
matplotlib==3.2.0
|
||||||
|
mccabe==0.6.1
|
||||||
|
more-itertools==8.2.0
|
||||||
|
multidict==4.7.5
|
||||||
|
musicbrainzngs==0.7.1
|
||||||
|
numpy==1.18.1
|
||||||
|
packaging==20.1
|
||||||
|
parso==0.6.2
|
||||||
|
pathspec==0.7.0
|
||||||
|
pbr==5.4.4
|
||||||
|
pillow==7.0.0
|
||||||
|
pluggy==0.13.1
|
||||||
|
progress==1.5
|
||||||
|
pudb==2019.2
|
||||||
|
py==1.8.1
|
||||||
|
pycodestyle==2.5.0
|
||||||
|
pydocstyle==5.0.2
|
||||||
|
pyflakes==2.1.1
|
||||||
|
pygments==2.5.2
|
||||||
|
pyls-black==0.4.4
|
||||||
|
pyparsing==2.4.6
|
||||||
|
pytest==5.3.5
|
||||||
|
python-dateutil==2.8.1
|
||||||
|
python-jsonrpc-server==0.3.4
|
||||||
|
python-language-server==0.31.8
|
||||||
|
pytz==2019.3
|
||||||
|
regex==2020.2.20
|
||||||
|
requests==2.23.0
|
||||||
|
rope==0.16.0
|
||||||
|
six==1.14.0
|
||||||
|
snowballstemmer==2.0.0
|
||||||
|
sphinx==2.4.4
|
||||||
|
sphinx-click==2.3.1
|
||||||
|
sphinx-rtd-theme==0.4.3
|
||||||
|
sphinxcontrib-applehelp==1.0.2
|
||||||
|
sphinxcontrib-devhelp==1.0.2
|
||||||
|
sphinxcontrib-htmlhelp==1.0.3
|
||||||
|
sphinxcontrib-jsmath==1.0.1
|
||||||
|
sphinxcontrib-qthelp==1.0.3
|
||||||
|
sphinxcontrib-serializinghtml==1.1.4
|
||||||
|
toml==0.10.0
|
||||||
|
typed-ast==1.4.1
|
||||||
|
ujson==1.35; platform_system != "Windows"
|
||||||
|
urllib3==1.25.8
|
||||||
|
urwid==2.1.0
|
||||||
|
wcwidth==0.1.8
|
||||||
|
wordcloud==1.6.0
|
||||||
|
yapf==0.29.0
|
||||||
|
zipp==3.0.0; python_version < "3.8"
|
||||||
19
requirements.txt
Normal file
19
requirements.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
addict==2.2.1
|
||||||
|
beautifultable==0.8.0
|
||||||
|
certifi==2019.11.28
|
||||||
|
chardet==3.0.4
|
||||||
|
cycler==0.10.0
|
||||||
|
idna==2.9
|
||||||
|
kiwisolver==1.1.0
|
||||||
|
matplotlib==3.2.0
|
||||||
|
multidict==4.7.5
|
||||||
|
musicbrainzngs==0.7.1
|
||||||
|
numpy==1.18.1
|
||||||
|
pillow==7.0.0
|
||||||
|
progress==1.5
|
||||||
|
pyparsing==2.4.6
|
||||||
|
python-dateutil==2.8.1
|
||||||
|
requests==2.23.0
|
||||||
|
six==1.14.0
|
||||||
|
urllib3==1.25.8
|
||||||
|
wordcloud==1.6.0
|
||||||
52
setup.py
Normal file
52
setup.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
package_dir = \
|
||||||
|
{'': 'src'}
|
||||||
|
|
||||||
|
packages = \
|
||||||
|
['musicbrainzapi',
|
||||||
|
'musicbrainzapi.api',
|
||||||
|
'musicbrainzapi.api.lyrics',
|
||||||
|
'musicbrainzapi.cli',
|
||||||
|
'musicbrainzapi.cli.commands',
|
||||||
|
'musicbrainzapi.wordcloud',
|
||||||
|
'musicbrainzapi.wordcloud.resources']
|
||||||
|
|
||||||
|
package_data = \
|
||||||
|
{'': ['*']}
|
||||||
|
|
||||||
|
install_requires = \
|
||||||
|
['addict>=2.2.1,<3.0.0',
|
||||||
|
'beautifultable>=0.8.0,<0.9.0',
|
||||||
|
'click>=7.0,<8.0',
|
||||||
|
'multidict>=4.7.5,<5.0.0',
|
||||||
|
'musicbrainzngs>=0.7.1,<0.8.0',
|
||||||
|
'numpy>=1.18.1,<2.0.0',
|
||||||
|
'progress>=1.5,<2.0',
|
||||||
|
'requests>=2.23.0,<3.0.0',
|
||||||
|
'wordcloud>=1.6.0,<2.0.0']
|
||||||
|
|
||||||
|
entry_points = \
|
||||||
|
{'console_scripts': ['musicbrainzapi = musicbrainzapi.cli.cli:cli']}
|
||||||
|
|
||||||
|
setup_kwargs = {
|
||||||
|
'name': 'musicbrainzapi',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'long_description': None,
|
||||||
|
'description': 'Python module to calculate statistics and generate a wordcloud for a given artist. Uses the Musicbrainz API and the lyrics.ovh API.',
|
||||||
|
'author': 'dtomlinson',
|
||||||
|
'author_email': 'dtomlinson@panaetius.co.uk',
|
||||||
|
'maintainer': None,
|
||||||
|
'maintainer_email': None,
|
||||||
|
'url': 'https://github.com/dtomlinson91/musicbrainzapi-cv-airelogic',
|
||||||
|
'package_dir': package_dir,
|
||||||
|
'packages': packages,
|
||||||
|
'package_data': package_data,
|
||||||
|
'install_requires': install_requires,
|
||||||
|
'entry_points': entry_points,
|
||||||
|
'python_requires': '>=3.7,<4.0',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setup(**setup_kwargs)
|
||||||
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
src/musicbrainzapi/.DS_Store
vendored
Normal file
BIN
src/musicbrainzapi/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1 +1,7 @@
|
|||||||
__version__ = '0.1.0'
|
"""
|
||||||
|
musicbrainzapi: A CLI lyrics searcher.
|
||||||
|
======================================
|
||||||
|
|
||||||
|
This module was written by dtomlinson <dtomlinson@panaetius.co.uk> for Aire Logic
|
||||||
|
|
||||||
|
"""
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = '0.1.0'
|
__version__ = '1.0.0'
|
||||||
|
|||||||
BIN
src/musicbrainzapi/api/.DS_Store
vendored
Normal file
BIN
src/musicbrainzapi/api/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
from . import lyrics
|
|
||||||
@@ -1,409 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
from abc import ABC, abstractmethod, abstractstaticmethod
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pprint import pprint
|
|
||||||
from typing import Union, List
|
|
||||||
import contextlib
|
|
||||||
import itertools
|
|
||||||
from progress.spinner import PieSpinner
|
|
||||||
|
|
||||||
# from pprint import pprint
|
|
||||||
|
|
||||||
import musicbrainzngs
|
|
||||||
import click
|
|
||||||
import addict
|
|
||||||
|
|
||||||
from musicbrainzapi.api import authenticate
|
|
||||||
|
|
||||||
|
|
||||||
class LyricsConcreteBuilder(ABC):
|
|
||||||
"""docstring for Lyrics"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def product(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def artist(self) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@artist.setter
|
|
||||||
@abstractmethod
|
|
||||||
def artist(self, artist: str) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def country(self) -> Union[str, None]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@country.setter
|
|
||||||
@abstractmethod
|
|
||||||
def country(self, country: Union[str, None]) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def artist_id(self) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@artist_id.setter
|
|
||||||
@abstractmethod
|
|
||||||
def artist_id(self, artist_id: str) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractstaticmethod
|
|
||||||
def set_useragent():
|
|
||||||
authenticate.set_useragent()
|
|
||||||
|
|
||||||
@abstractstaticmethod
|
|
||||||
def browse_releases(self) -> dict:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractstaticmethod
|
|
||||||
def get_album_info_list(self) -> list:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractstaticmethod
|
|
||||||
def paginate_results(self) -> list:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def __init__(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def reset(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def do_search_artists(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def do_sort_names(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_accuracy_scores(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_top_five_results(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def do_search_albums(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class LyricsBuilder(LyricsConcreteBuilder):
|
|
||||||
"""docstring for LyricsBuilder"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def product(self) -> Lyrics:
|
|
||||||
product = self._product
|
|
||||||
return product
|
|
||||||
|
|
||||||
@property
|
|
||||||
def artist(self) -> str:
|
|
||||||
return self._artist
|
|
||||||
|
|
||||||
@artist.setter
|
|
||||||
def artist(self, artist: str) -> None:
|
|
||||||
self._artist = artist
|
|
||||||
self._product.artist = artist
|
|
||||||
|
|
||||||
@property
|
|
||||||
def country(self) -> Union[str, None]:
|
|
||||||
return self._country
|
|
||||||
|
|
||||||
@country.setter
|
|
||||||
def country(self, country: Union[str, None]) -> None:
|
|
||||||
self._country = country
|
|
||||||
self._product.country = country
|
|
||||||
|
|
||||||
@property
|
|
||||||
def artist_id(self) -> str:
|
|
||||||
return self._artist_id
|
|
||||||
|
|
||||||
@artist_id.setter
|
|
||||||
def artist_id(self, artist_id: str) -> None:
|
|
||||||
self._artist_id = artist_id
|
|
||||||
self._product.artist_id = artist_id
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all_tracks(self) -> set:
|
|
||||||
return self._all_tracks
|
|
||||||
|
|
||||||
@all_tracks.setter
|
|
||||||
def all_tracks(self, all_tracks: set) -> None:
|
|
||||||
self._all_tracks = all_tracks
|
|
||||||
self._product.all_tracks = all_tracks
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all_albums_with_tracks(self) -> list:
|
|
||||||
return self._all_albums_with_tracks
|
|
||||||
|
|
||||||
@all_albums_with_tracks.setter
|
|
||||||
def all_albums_with_tracks(self, all_albums_with_tracks: list) -> None:
|
|
||||||
self._all_albums_with_tracks = all_albums_with_tracks
|
|
||||||
self._product.all_albums_with_tracks = all_albums_with_tracks
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_useragent() -> None:
|
|
||||||
authenticate.set_useragent()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def browse_releases(
|
|
||||||
artist_id: str,
|
|
||||||
limit: int,
|
|
||||||
release_type: list,
|
|
||||||
offset: Union[int, None] = None,
|
|
||||||
includes: Union[List[str, None]] = list(),
|
|
||||||
) -> dict:
|
|
||||||
releases = musicbrainzngs.browse_releases(
|
|
||||||
artist=artist_id,
|
|
||||||
limit=limit,
|
|
||||||
release_type=release_type,
|
|
||||||
offset=offset,
|
|
||||||
includes=includes,
|
|
||||||
)
|
|
||||||
return releases
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_album_info_list(
|
|
||||||
album_info_list: list, album_tracker: set, releases: addict.Dict
|
|
||||||
) -> list:
|
|
||||||
for i in releases['release-list']:
|
|
||||||
_throwaway_dict = addict.Dict()
|
|
||||||
_throwaway_dict.album = i.title
|
|
||||||
_throwaway_dict.year = i.date.split('-')[0]
|
|
||||||
_throwaway_dict.tracks = [
|
|
||||||
j.recording.title for j in i['medium-list'][0]['track-list']
|
|
||||||
]
|
|
||||||
if i.title not in album_tracker:
|
|
||||||
album_tracker.add(i.title)
|
|
||||||
album_info_list.append(_throwaway_dict)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
return album_info_list, album_tracker
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def paginate_results(
|
|
||||||
releases: addict.Dict, duplicated_tracks: list
|
|
||||||
) -> List:
|
|
||||||
tracks = [
|
|
||||||
j.recording.title
|
|
||||||
for i in releases['release-list']
|
|
||||||
for j in i['medium-list'][0]['track-list']
|
|
||||||
]
|
|
||||||
for i in itertools.chain(tracks):
|
|
||||||
duplicated_tracks.append(i)
|
|
||||||
return duplicated_tracks
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self._product = Lyrics
|
|
||||||
|
|
||||||
def do_search_artists(self) -> None:
|
|
||||||
self.musicbrainz_artists = musicbrainzngs.search_artists(
|
|
||||||
artist=self.artist, country=self.country
|
|
||||||
)
|
|
||||||
# pprint(self.musicbrainz_artists['artist-list'])
|
|
||||||
return self
|
|
||||||
|
|
||||||
def do_sort_names(self) -> None:
|
|
||||||
self._sort_names = dict(
|
|
||||||
(i.get('id'), f'{i.get("sort-name")} | {i.get("disambiguation")}')
|
|
||||||
if i.get('disambiguation') is not None
|
|
||||||
else (i.get('id'), f'{i.get("sort-name")}')
|
|
||||||
for i in self.musicbrainz_artists['artist-list']
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def get_accuracy_scores(self) -> None:
|
|
||||||
self._accuracy_scores = dict(
|
|
||||||
(i.get('id'), int(i.get('ext:score', '0')))
|
|
||||||
for i in self.musicbrainz_artists['artist-list']
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def get_top_five_results(self) -> None:
|
|
||||||
self._top_five_results = dict(
|
|
||||||
(i, self._accuracy_scores.get(i))
|
|
||||||
for i in sorted(
|
|
||||||
self._accuracy_scores,
|
|
||||||
key=self._accuracy_scores.get,
|
|
||||||
reverse=True,
|
|
||||||
)[0:5]
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def do_search_albums(self) -> None:
|
|
||||||
album_info_list = list()
|
|
||||||
album_tracker = set()
|
|
||||||
duplicated_tracks = list()
|
|
||||||
limit, offset, page = (100, 0, 1)
|
|
||||||
|
|
||||||
raw_releases = self.browse_releases(
|
|
||||||
artist_id=self.artist_id,
|
|
||||||
limit=limit,
|
|
||||||
release_type=['album'],
|
|
||||||
offset=offset,
|
|
||||||
includes=['recordings'],
|
|
||||||
)
|
|
||||||
|
|
||||||
releases = addict.Dict(raw_releases)
|
|
||||||
|
|
||||||
duplicated_tracks = self.paginate_results(releases, duplicated_tracks)
|
|
||||||
|
|
||||||
# Get album info list
|
|
||||||
album_info_list, album_tracker = self.get_album_info_list(
|
|
||||||
album_info_list, album_tracker, releases
|
|
||||||
)
|
|
||||||
|
|
||||||
bar_count = len(releases['release-list'])
|
|
||||||
previous_bar_count = 0
|
|
||||||
|
|
||||||
with PieSpinner(
|
|
||||||
f'Finding all tracks in all albums for {self.artist}'
|
|
||||||
'from Musicbrainz '
|
|
||||||
) as bar:
|
|
||||||
while bar_count != previous_bar_count:
|
|
||||||
offset += limit
|
|
||||||
page += 1
|
|
||||||
# Get next page
|
|
||||||
raw_page_releases = self.browse_releases(
|
|
||||||
artist_id=self.artist_id,
|
|
||||||
limit=limit,
|
|
||||||
release_type=['album'],
|
|
||||||
offset=offset,
|
|
||||||
includes=['recordings'],
|
|
||||||
)
|
|
||||||
page_releases = addict.Dict(raw_page_releases)
|
|
||||||
|
|
||||||
# Update list
|
|
||||||
duplicated_tracks = self.paginate_results(
|
|
||||||
page_releases, duplicated_tracks
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update album info list
|
|
||||||
(
|
|
||||||
album_info_list,
|
|
||||||
album_tracker,
|
|
||||||
) = self.get_album_info_list(
|
|
||||||
album_info_list, album_tracker, releases
|
|
||||||
)
|
|
||||||
|
|
||||||
previous_bar_count = bar_count
|
|
||||||
bar_count += len(page_releases['release-list'])
|
|
||||||
bar.next()
|
|
||||||
total_releases_count = bar_count
|
|
||||||
|
|
||||||
tracks = set(duplicated_tracks)
|
|
||||||
|
|
||||||
click.echo(
|
|
||||||
f'Musicbrainz found {len(tracks)} unique tracks in '
|
|
||||||
f'{total_releases_count} releases for {self.artist}'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set properties
|
|
||||||
self.all_tracks = tracks
|
|
||||||
self.all_albums_with_tracks = album_info_list
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class LyricsClickDirector:
|
|
||||||
"""docstring for LyricsClickDirector"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._builder = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def builder(self) -> LyricsBuilder:
|
|
||||||
return self._builder
|
|
||||||
|
|
||||||
@builder.setter
|
|
||||||
def builder(self, builder: LyricsBuilder) -> None:
|
|
||||||
self._builder = builder
|
|
||||||
|
|
||||||
def _get_initial_artists(self, artist: str, country: str) -> None:
|
|
||||||
self.builder.artist = artist
|
|
||||||
self.builder.country = country
|
|
||||||
self.builder.set_useragent()
|
|
||||||
self.builder.do_search_artists()
|
|
||||||
self.builder.do_sort_names()
|
|
||||||
self.builder.get_accuracy_scores()
|
|
||||||
self.builder.get_top_five_results()
|
|
||||||
|
|
||||||
def _confirm_final_artist(self) -> None:
|
|
||||||
artist_meta = None
|
|
||||||
for i, j in self.builder._top_five_results.items():
|
|
||||||
artist_meta = 'Multiple' if j <= 100 else None
|
|
||||||
|
|
||||||
if artist_meta == 'Multiple':
|
|
||||||
_position = []
|
|
||||||
click.echo(
|
|
||||||
f'Musicbrainz found several results for '
|
|
||||||
f'{self.builder.artist[0]}. Which artist/group do you want?',
|
|
||||||
)
|
|
||||||
for i, j in zip(self.builder._top_five_results, range(1, 6)):
|
|
||||||
click.echo(
|
|
||||||
f'[{j}] {self.builder._sort_names.get(i)}'
|
|
||||||
f' ({self.builder._accuracy_scores.get(i)}% match)'
|
|
||||||
)
|
|
||||||
_position.append(i)
|
|
||||||
chosen = int(
|
|
||||||
click.prompt(
|
|
||||||
f'Enter choice, default is',
|
|
||||||
default=1,
|
|
||||||
type=click.IntRange(
|
|
||||||
1, len(self.builder._top_five_results)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
choice = _position[chosen - 1]
|
|
||||||
click.echo(f'You chose {self.builder._sort_names.get(choice)}')
|
|
||||||
self._artist = self.builder._sort_names.get(choice).split('|')[0]
|
|
||||||
self._artist_id = choice
|
|
||||||
|
|
||||||
elif artist_meta is None:
|
|
||||||
click.echo(
|
|
||||||
f'Musicbrainz did not find any results for '
|
|
||||||
f'{self.builder.artist[0]}. Check the spelling or consider '
|
|
||||||
'alternative names that the artist/group may go by.'
|
|
||||||
)
|
|
||||||
raise SystemExit()
|
|
||||||
|
|
||||||
def _search_for_all_tracks(self) -> None:
|
|
||||||
self.builder.do_search_albums()
|
|
||||||
pprint(self.builder._product.all_tracks)
|
|
||||||
# pprint(self.builder._product.all_albums_with_tracks)
|
|
||||||
|
|
||||||
def _set_artist_id_on_product(self) -> None:
|
|
||||||
self.builder.artist_id = self._artist_id
|
|
||||||
self.builder.artist = self._artist
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Lyrics:
|
|
||||||
"""docstring for Lyrics"""
|
|
||||||
|
|
||||||
__slots__ = [
|
|
||||||
'artist_id',
|
|
||||||
'artist',
|
|
||||||
'country',
|
|
||||||
'all_tracks',
|
|
||||||
'all_albums_with_tracks',
|
|
||||||
]
|
|
||||||
artist_id: str
|
|
||||||
artist: str
|
|
||||||
country: Union[str, None]
|
|
||||||
all_tracks: set
|
|
||||||
all_albums_with_tracks: list
|
|
||||||
135
src/musicbrainzapi/api/lyrics/__init__.py
Normal file
135
src/musicbrainzapi/api/lyrics/__init__.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
"""
|
||||||
|
Lyrics object with statistics.
|
||||||
|
===============================
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Union, Dict, List
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import math
|
||||||
|
|
||||||
|
from beautifultable import BeautifulTable
|
||||||
|
import click
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Lyrics:
|
||||||
|
"""Lyrics object for an artist."""
|
||||||
|
|
||||||
|
artist_id: str
|
||||||
|
artist: str
|
||||||
|
country: Union[str, None]
|
||||||
|
all_albums_with_tracks: List[Dict[str, List[str]]]
|
||||||
|
all_albums_with_lyrics: List[Dict[str, List[str]]]
|
||||||
|
all_albums_lyrics_count: List[Dict[str, List[List[str, int]]]]
|
||||||
|
all_albums_lyrics_sum: List[Dict[str, List[int, str]]]
|
||||||
|
album_statistics: Dict[str, Dict[str, int]]
|
||||||
|
year_statistics: Dict[str, Dict[str, int]]
|
||||||
|
|
||||||
|
_attributes = [
|
||||||
|
'all_albums_with_tracks',
|
||||||
|
'all_albums_with_lyrics',
|
||||||
|
'all_albums_lyrics_count',
|
||||||
|
'all_albums_lyrics_sum',
|
||||||
|
'album_statistics',
|
||||||
|
'year_statistics',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def show_summary(self) -> None:
|
||||||
|
"""Show the average word count for all lyrics
|
||||||
|
"""
|
||||||
|
all_averages = []
|
||||||
|
|
||||||
|
for i in self.album_statistics.values():
|
||||||
|
try:
|
||||||
|
all_averages.append(i['avg'])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
# print(all_averages)
|
||||||
|
try:
|
||||||
|
final_average = math.ceil(np.mean(all_averages))
|
||||||
|
except ValueError:
|
||||||
|
click.echo(
|
||||||
|
'Oops! https://lyrics.ovh couldn\'t find any lyrics across any'
|
||||||
|
' album. This is caused by inconsistent Artist names from'
|
||||||
|
' Musicbrainz and lyrics.ovh. Try another artist.'
|
||||||
|
)
|
||||||
|
raise (SystemExit)
|
||||||
|
output = BeautifulTable(max_width=200)
|
||||||
|
output.set_style(BeautifulTable.STYLE_BOX_ROUNDED)
|
||||||
|
output.column_headers = [
|
||||||
|
'Average number of words in tracks across all albums\n'
|
||||||
|
f'for {self.artist}'
|
||||||
|
]
|
||||||
|
output.append_row([final_average])
|
||||||
|
click.echo(output)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def show_summary_statistics(self, group_by: str) -> None:
|
||||||
|
"""Summary
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
group_by : str
|
||||||
|
Parameter to group statistics by. Valid options are album or year
|
||||||
|
"""
|
||||||
|
stats_obj = getattr(self, f'{group_by}_statistics')
|
||||||
|
stats = [
|
||||||
|
'avg',
|
||||||
|
'std',
|
||||||
|
'min',
|
||||||
|
'max',
|
||||||
|
'median',
|
||||||
|
'count',
|
||||||
|
'p_10',
|
||||||
|
'p_25',
|
||||||
|
'p_75',
|
||||||
|
'p_90',
|
||||||
|
]
|
||||||
|
output_0 = BeautifulTable(max_width=200)
|
||||||
|
output_0.set_style(BeautifulTable.STYLE_BOX_ROUNDED)
|
||||||
|
output_0.column_headers = [
|
||||||
|
'Descriptive statistics for number of words in tracks across all'
|
||||||
|
f' {group_by}s\nfor {self.artist}'
|
||||||
|
]
|
||||||
|
output_1 = BeautifulTable(max_width=200)
|
||||||
|
output_1.set_style(BeautifulTable.STYLE_BOX_ROUNDED)
|
||||||
|
output_1.column_headers = [
|
||||||
|
group_by,
|
||||||
|
stats[0],
|
||||||
|
stats[1],
|
||||||
|
stats[2],
|
||||||
|
stats[3],
|
||||||
|
stats[4],
|
||||||
|
stats[5],
|
||||||
|
stats[6],
|
||||||
|
stats[7],
|
||||||
|
stats[8],
|
||||||
|
stats[9],
|
||||||
|
]
|
||||||
|
for group, s in stats_obj.items():
|
||||||
|
try:
|
||||||
|
output_1.append_row(
|
||||||
|
[
|
||||||
|
group,
|
||||||
|
s.get(stats[0]),
|
||||||
|
s.get(stats[1]),
|
||||||
|
s.get(stats[2]),
|
||||||
|
s.get(stats[3]),
|
||||||
|
s.get(stats[4]),
|
||||||
|
s.get(stats[5]),
|
||||||
|
s.get(stats[6]),
|
||||||
|
s.get(stats[7]),
|
||||||
|
s.get(stats[8]),
|
||||||
|
s.get(stats[9]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
output_0.append_row([output_1])
|
||||||
|
click.echo(output_0)
|
||||||
|
return self
|
||||||
520
src/musicbrainzapi/api/lyrics/builder.py
Normal file
520
src/musicbrainzapi/api/lyrics/builder.py
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from collections import Counter
|
||||||
|
import html
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import string
|
||||||
|
from typing import Union, Dict
|
||||||
|
|
||||||
|
import addict
|
||||||
|
import click
|
||||||
|
import musicbrainzngs
|
||||||
|
import numpy as np
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from musicbrainzapi.api.lyrics.concrete_builder import LyricsConcreteBuilder
|
||||||
|
from musicbrainzapi.api.lyrics import Lyrics
|
||||||
|
from musicbrainzapi.api import authenticate
|
||||||
|
|
||||||
|
|
||||||
|
class LyricsBuilder(LyricsConcreteBuilder):
|
||||||
|
"""docstring for LyricsBuilder
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
album_statistics : addict.Dict
|
||||||
|
Dictionary containing album statistics
|
||||||
|
all_albums : list
|
||||||
|
List of all albums + track titles
|
||||||
|
all_albums_lyrics : list
|
||||||
|
List of all albums + track lyrics
|
||||||
|
all_albums_lyrics_count : list
|
||||||
|
List of all albums + track lyrics counted by each word
|
||||||
|
all_albums_lyrics_sum : list
|
||||||
|
List of all albums + track lyrics counted and summed up.
|
||||||
|
all_albums_lyrics_url : list
|
||||||
|
List of all albums + link to lyrics api for each track.
|
||||||
|
musicbrainz_artists : addict.Dict
|
||||||
|
Dictionary of response from Musicbrainzapi
|
||||||
|
release_group_ids : addict.Dict
|
||||||
|
Dictionary of Musicbrainz release-group ids
|
||||||
|
total_track_count : int
|
||||||
|
Total number of tracks across all albums
|
||||||
|
year_statistics : addict.Dict
|
||||||
|
Dictionary containing album statistics
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def product(self) -> Lyrics:
|
||||||
|
product = self._product
|
||||||
|
return product
|
||||||
|
|
||||||
|
@property
|
||||||
|
def artist(self) -> str:
|
||||||
|
return self._artist
|
||||||
|
|
||||||
|
@artist.setter
|
||||||
|
def artist(self, artist: str) -> None:
|
||||||
|
self._artist = artist
|
||||||
|
self._product.artist = artist
|
||||||
|
|
||||||
|
@property
|
||||||
|
def country(self) -> Union[str, None]:
|
||||||
|
return self._country
|
||||||
|
|
||||||
|
@country.setter
|
||||||
|
def country(self, country: Union[str, None]) -> None:
|
||||||
|
self._country = country
|
||||||
|
self._product.country = country
|
||||||
|
|
||||||
|
@property
|
||||||
|
def artist_id(self) -> str:
|
||||||
|
return self._artist_id
|
||||||
|
|
||||||
|
@artist_id.setter
|
||||||
|
def artist_id(self, artist_id: str) -> None:
|
||||||
|
self._artist_id = artist_id
|
||||||
|
self._product.artist_id = artist_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_albums_with_tracks(self) -> list:
|
||||||
|
return self._all_albums_with_tracks
|
||||||
|
|
||||||
|
@all_albums_with_tracks.setter
|
||||||
|
def all_albums_with_tracks(self, all_albums_with_tracks: list) -> None:
|
||||||
|
self._all_albums_with_tracks = all_albums_with_tracks
|
||||||
|
self._product.all_albums_with_tracks = all_albums_with_tracks
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_useragent() -> None:
|
||||||
|
"""Sets the useragent for the Musicbrainz api.
|
||||||
|
"""
|
||||||
|
authenticate.set_useragent()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def construct_lyrics_url(artist: str, song: str) -> str:
|
||||||
|
"""Builds the URL for the lyrics api.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
artist : str
|
||||||
|
Artist
|
||||||
|
song : str
|
||||||
|
Track title
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
URL for lyrics from the lyrics api.
|
||||||
|
|
||||||
|
"""
|
||||||
|
lyrics_api_base = 'https://api.lyrics.ovh/v1'
|
||||||
|
lyrics_api_url = html.escape(f'{lyrics_api_base}/{artist}/{song}')
|
||||||
|
return lyrics_api_url
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def request_lyrics_from_url(url: str) -> str:
|
||||||
|
"""Gets lyrics from the lyrics api.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
url : str
|
||||||
|
URL of the track for the lyrics api.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
Lyrics of the track.
|
||||||
|
|
||||||
|
"""
|
||||||
|
resp = requests.get(url)
|
||||||
|
|
||||||
|
# No lyrics for a song will return a key of 'error', we pass on this.
|
||||||
|
try:
|
||||||
|
lyrics = LyricsBuilder.strip_punctuation(resp.json()['lyrics'])
|
||||||
|
return lyrics
|
||||||
|
except (KeyError, json.decoder.JSONDecodeError):
|
||||||
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def strip_punctuation(word: str) -> str:
|
||||||
|
"""Removes punctuation from lyrics.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
word : str
|
||||||
|
Word to remove punctuation from.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
Same word without any punctuation.
|
||||||
|
"""
|
||||||
|
_strip = word.translate(str.maketrans('', '', string.punctuation))
|
||||||
|
return _strip
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_descriptive_statistics(nums: list) -> Dict[str, int]:
|
||||||
|
"""Calculates descriptive statistics.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
nums : list
|
||||||
|
A list containing total number of words from a track.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Dict[str, int]
|
||||||
|
Dictionary of statistic and value.
|
||||||
|
"""
|
||||||
|
if len(nums) == 0:
|
||||||
|
return
|
||||||
|
avg = math.ceil(np.mean(nums))
|
||||||
|
median = math.ceil(np.median(nums))
|
||||||
|
std = math.ceil(np.std(nums))
|
||||||
|
_max = math.ceil(np.max(nums))
|
||||||
|
_min = math.ceil(np.min(nums))
|
||||||
|
p_10 = math.ceil(np.percentile(nums, 10))
|
||||||
|
p_25 = math.ceil(np.percentile(nums, 25))
|
||||||
|
p_75 = math.ceil(np.percentile(nums, 75))
|
||||||
|
p_90 = math.ceil(np.percentile(nums, 90))
|
||||||
|
count = len(nums)
|
||||||
|
_d = addict.Dict(
|
||||||
|
('avg', avg),
|
||||||
|
('median', median),
|
||||||
|
('std', std),
|
||||||
|
('max', _max),
|
||||||
|
('min', _min),
|
||||||
|
('p_10', p_10),
|
||||||
|
('p_25', p_25),
|
||||||
|
('p_75', p_75),
|
||||||
|
('p_90', p_90),
|
||||||
|
('count', count),
|
||||||
|
)
|
||||||
|
return _d
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Create a builder instance to build a Lyrics object."""
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset the builder and create new product.
|
||||||
|
"""
|
||||||
|
self._product = Lyrics()
|
||||||
|
|
||||||
|
def find_artists(self) -> None:
|
||||||
|
"""Find artists from the musicbrainz api
|
||||||
|
"""
|
||||||
|
self.musicbrainz_artists = musicbrainzngs.search_artists(
|
||||||
|
artist=self.artist, country=self.country
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def sort_artists(self) -> None:
|
||||||
|
"""Sort the artists from the Musicbrainzapi."""
|
||||||
|
self._sort_names = dict(
|
||||||
|
(i.get('id'), f'{i.get("name")} | {i.get("disambiguation")}')
|
||||||
|
if i.get('disambiguation') is not None
|
||||||
|
else (i.get('id'), f'{i.get("name")}')
|
||||||
|
for i in self.musicbrainz_artists['artist-list']
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_accuracy_scores(self) -> None:
|
||||||
|
"""Get accuracy scores from the Musicbrainzapi
|
||||||
|
"""
|
||||||
|
self._accuracy_scores = dict(
|
||||||
|
(i.get('id'), int(i.get('ext:score', '0')))
|
||||||
|
for i in self.musicbrainz_artists['artist-list']
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_top_five_results(self) -> None:
|
||||||
|
"""Get the top five artists from the Musicbrainzapi
|
||||||
|
"""
|
||||||
|
self._top_five_results = dict(
|
||||||
|
(i, self._accuracy_scores.get(i))
|
||||||
|
for i in sorted(
|
||||||
|
self._accuracy_scores,
|
||||||
|
key=self._accuracy_scores.get,
|
||||||
|
reverse=True,
|
||||||
|
)[0:5]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def find_all_albums(self) -> None:
|
||||||
|
"""Find all albums for the chosen artist."""
|
||||||
|
limit, offset, page = (100, 0, 1)
|
||||||
|
|
||||||
|
resp_0 = addict.Dict(
|
||||||
|
musicbrainzngs.browse_release_groups(
|
||||||
|
artist=self.artist_id, release_type=['album'], limit=limit
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
total_releases = resp_0['release-group-count']
|
||||||
|
response_releases = len(resp_0['release-group-list'])
|
||||||
|
|
||||||
|
with click.progressbar(
|
||||||
|
length=total_releases,
|
||||||
|
label=f'Searching Musicbrainz for all albums from {self.artist}',
|
||||||
|
) as bar:
|
||||||
|
|
||||||
|
release_group_ids = addict.Dict(
|
||||||
|
(i.id, i.title)
|
||||||
|
for i in resp_0['release-group-list']
|
||||||
|
if i.type == 'Album'
|
||||||
|
)
|
||||||
|
|
||||||
|
bar.update(response_releases)
|
||||||
|
|
||||||
|
while response_releases > 0:
|
||||||
|
# Get next page
|
||||||
|
offset += limit
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
resp_1 = addict.Dict(
|
||||||
|
musicbrainzngs.browse_release_groups(
|
||||||
|
artist=self.artist_id,
|
||||||
|
release_type=['album'],
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
response_releases = len(resp_1['release-group-list'])
|
||||||
|
|
||||||
|
release_group_ids = addict.Dict(
|
||||||
|
**release_group_ids,
|
||||||
|
**addict.Dict(
|
||||||
|
(i.id, i.title)
|
||||||
|
for i in resp_1['release-group-list']
|
||||||
|
if i.type == 'Album'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bar.update(response_releases)
|
||||||
|
|
||||||
|
self.release_group_ids = release_group_ids
|
||||||
|
click.echo(f'Found {len(release_group_ids)} albums for {self.artist}.')
|
||||||
|
|
||||||
|
del (resp_0, resp_1)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def find_all_tracks(self) -> None:
|
||||||
|
"""Find all tracks from all albums.
|
||||||
|
"""
|
||||||
|
self.all_albums = list()
|
||||||
|
total_albums = len(self.release_group_ids)
|
||||||
|
self.total_track_count = 0
|
||||||
|
|
||||||
|
with click.progressbar(
|
||||||
|
length=total_albums,
|
||||||
|
label=(
|
||||||
|
'Searching Musicbrainz for all tracks in all albums for '
|
||||||
|
f'{self.artist}'
|
||||||
|
),
|
||||||
|
) as bar:
|
||||||
|
for _id, alb in self.release_group_ids.items():
|
||||||
|
resp_0 = addict.Dict(
|
||||||
|
musicbrainzngs.browse_releases(
|
||||||
|
release_group=_id,
|
||||||
|
release_type=['album'],
|
||||||
|
includes=['recordings'],
|
||||||
|
limit=100,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
album_track_count = [
|
||||||
|
i['medium-list'][0]['track-count']
|
||||||
|
for i in resp_0['release-list']
|
||||||
|
]
|
||||||
|
|
||||||
|
self.total_track_count += max(album_track_count)
|
||||||
|
|
||||||
|
max_track_pos = album_track_count.index(max(album_track_count))
|
||||||
|
|
||||||
|
album_tracks = resp_0['release-list'][max_track_pos]
|
||||||
|
|
||||||
|
try:
|
||||||
|
album_year = resp_0['release-list'][
|
||||||
|
max_track_pos
|
||||||
|
].date.split('-')[0]
|
||||||
|
except TypeError:
|
||||||
|
album_year = 'Missing'
|
||||||
|
|
||||||
|
album_tracks = addict.Dict(
|
||||||
|
(
|
||||||
|
alb + f' [{album_year}]',
|
||||||
|
[
|
||||||
|
i.recording.title
|
||||||
|
for i in resp_0['release-list'][max_track_pos][
|
||||||
|
'medium-list'
|
||||||
|
][0]['track-list']
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.all_albums.append(album_tracks)
|
||||||
|
|
||||||
|
bar.update(1)
|
||||||
|
|
||||||
|
# pprint(self.all_albums)
|
||||||
|
click.echo(
|
||||||
|
f'Found {self.total_track_count} tracks across'
|
||||||
|
f' {len(self.release_group_ids)} albums for {self.artist}'
|
||||||
|
)
|
||||||
|
del resp_0
|
||||||
|
return self
|
||||||
|
|
||||||
|
def find_lyrics_urls(self) -> None:
|
||||||
|
"""Construct the URL for the lyrics api."""
|
||||||
|
self.all_albums_lyrics_url = list()
|
||||||
|
for x in self.all_albums:
|
||||||
|
for alb, tracks in x.items():
|
||||||
|
lyrics = addict.Dict(
|
||||||
|
(
|
||||||
|
alb,
|
||||||
|
[
|
||||||
|
self.construct_lyrics_url(self.artist, i)
|
||||||
|
for i in tracks
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.all_albums_lyrics_url.append(lyrics)
|
||||||
|
|
||||||
|
# pprint(self.all_albums_lyrics_url)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def find_all_lyrics(self) -> None:
|
||||||
|
"""Get lyrics for each track from the lyrics api
|
||||||
|
"""
|
||||||
|
self.all_albums_lyrics = list()
|
||||||
|
|
||||||
|
with click.progressbar(
|
||||||
|
length=self.total_track_count,
|
||||||
|
label=f'Finding lyrics for {self.total_track_count}'
|
||||||
|
f' tracks for {self.artist}. This may take some time! ☕️',
|
||||||
|
) as bar:
|
||||||
|
bar.update(5)
|
||||||
|
for x in self.all_albums_lyrics_url:
|
||||||
|
for alb, urls in x.items():
|
||||||
|
# bar.update(1)
|
||||||
|
update = len(urls)
|
||||||
|
lyrics = addict.Dict(
|
||||||
|
(alb, [self.request_lyrics_from_url(i) for i in urls])
|
||||||
|
)
|
||||||
|
self.all_albums_lyrics.append(lyrics)
|
||||||
|
bar.update(update)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def count_words_in_lyrics(self) -> None:
|
||||||
|
"""Count all words in each track
|
||||||
|
"""
|
||||||
|
self.all_albums_lyrics_count = list()
|
||||||
|
# print(self.total_track_count)
|
||||||
|
with click.progressbar(
|
||||||
|
length=self.total_track_count, label=f'Processing lyrics'
|
||||||
|
) as bar:
|
||||||
|
for x in self.all_albums_lyrics:
|
||||||
|
for alb, lyrics in x.items():
|
||||||
|
update = len(lyrics)
|
||||||
|
bar.update(1)
|
||||||
|
lyrics = addict.Dict(
|
||||||
|
(
|
||||||
|
alb,
|
||||||
|
[
|
||||||
|
Counter(i.split()).most_common()
|
||||||
|
if i is not None
|
||||||
|
else 'No Lyrics'
|
||||||
|
for i in lyrics
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.all_albums_lyrics_count.append(lyrics)
|
||||||
|
bar.update(update - 1)
|
||||||
|
click.echo(f'Processed lyrics for {self.total_track_count} tracks.')
|
||||||
|
return self
|
||||||
|
|
||||||
|
def calculate_track_totals(self) -> None:
|
||||||
|
"""Calculates total words for each track across all albums.
|
||||||
|
"""
|
||||||
|
self.all_albums_lyrics_sum = list()
|
||||||
|
album_lyrics = self.all_albums_lyrics_count
|
||||||
|
# with open(f'{os.getcwd()}/lyrics_count.json', 'r') as f:
|
||||||
|
# album_lyrics = json.load(f)
|
||||||
|
count = 0
|
||||||
|
for i in album_lyrics:
|
||||||
|
count += len(i)
|
||||||
|
for album, lyrics_list in i.items():
|
||||||
|
album_avg = list()
|
||||||
|
d = addict.Dict()
|
||||||
|
for j in lyrics_list:
|
||||||
|
if j != 'No Lyrics':
|
||||||
|
song_total = 0
|
||||||
|
for k in j:
|
||||||
|
song_total += k[1]
|
||||||
|
else:
|
||||||
|
song_total = "No Lyrics"
|
||||||
|
album_avg.append(song_total)
|
||||||
|
# We want to avoid a ValueError when we loop through
|
||||||
|
# the first time
|
||||||
|
try:
|
||||||
|
d = addict.Dict(**d, **addict.Dict(album, album_avg))
|
||||||
|
except ValueError:
|
||||||
|
d = addict.Dict((album, album_avg))
|
||||||
|
# print(d)
|
||||||
|
self.all_albums_lyrics_sum.append(d)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def calculate_final_average_by_album(self) -> None:
|
||||||
|
"""Calculates descriptive statistics by album.
|
||||||
|
"""
|
||||||
|
self.album_statistics = addict.Dict()
|
||||||
|
album_lyrics = self.all_albums_lyrics_sum
|
||||||
|
# with open(f'{os.getcwd()}/lyrics_sum_all_album.json', 'r') as f:
|
||||||
|
# album_lyrics = json.load(f)
|
||||||
|
|
||||||
|
for i in album_lyrics:
|
||||||
|
for album, count in i.items():
|
||||||
|
# We filter twice, once to remove strings, then to filter
|
||||||
|
# the integers
|
||||||
|
_count = [d for d in count if isinstance(d, int)]
|
||||||
|
_count = [d for d in _count if d > 1]
|
||||||
|
_d = self.get_descriptive_statistics(_count)
|
||||||
|
self.album_statistics = addict.Dict(
|
||||||
|
**self.album_statistics, **addict.Dict((album, _d))
|
||||||
|
)
|
||||||
|
# with open(f'{os.getcwd()}/album_statistics.json', 'w') as f:
|
||||||
|
# json.dump(self.album_statistics, f, indent=2)
|
||||||
|
# pprint(self.album_statistics)
|
||||||
|
|
||||||
|
def calculate_final_average_by_year(self) -> None:
|
||||||
|
"""Calculates descriptive statistic by year.
|
||||||
|
"""
|
||||||
|
group_by_years = addict.Dict()
|
||||||
|
self.year_statistics = addict.Dict()
|
||||||
|
album_lyrics = self.all_albums_lyrics_sum
|
||||||
|
# with open(f'{os.getcwd()}/lyrics_sum_all_album.json', 'r') as f:
|
||||||
|
# album_lyrics = json.load(f)
|
||||||
|
|
||||||
|
# Merge years together
|
||||||
|
for i in album_lyrics:
|
||||||
|
for album, count in i.items():
|
||||||
|
year = album.split('[')[-1].strip(']')
|
||||||
|
try:
|
||||||
|
group_by_years = addict.Dict(
|
||||||
|
**group_by_years, **addict.Dict((year, count))
|
||||||
|
)
|
||||||
|
# First loop returns value error for empty dict
|
||||||
|
except ValueError:
|
||||||
|
group_by_years = addict.Dict((year, count))
|
||||||
|
# Multiple years raise a TypeError - we append
|
||||||
|
except TypeError:
|
||||||
|
group_by_years.get(year).extend(count)
|
||||||
|
|
||||||
|
for year, y_count in group_by_years.items():
|
||||||
|
_y_count = [d for d in y_count if isinstance(d, int)]
|
||||||
|
_y_count = [d for d in _y_count if d > 1]
|
||||||
|
_d = self.get_descriptive_statistics(_y_count)
|
||||||
|
self.year_statistics = addict.Dict(
|
||||||
|
**self.year_statistics, **addict.Dict((year, _d))
|
||||||
|
)
|
||||||
119
src/musicbrainzapi/api/lyrics/concrete_builder.py
Normal file
119
src/musicbrainzapi/api/lyrics/concrete_builder.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from abc import ABC, abstractstaticmethod, abstractmethod
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
class LyricsConcreteBuilder(ABC):
|
||||||
|
"""Abstract concrete builder for Lyrics
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def product(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def artist(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@artist.setter
|
||||||
|
@abstractmethod
|
||||||
|
def artist(self, artist: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def country(self) -> Union[str, None]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@country.setter
|
||||||
|
@abstractmethod
|
||||||
|
def country(self, country: Union[str, None]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def artist_id(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@artist_id.setter
|
||||||
|
@abstractmethod
|
||||||
|
def artist_id(self, artist_id: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractstaticmethod
|
||||||
|
def set_useragent() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractstaticmethod
|
||||||
|
def construct_lyrics_url() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractstaticmethod
|
||||||
|
def request_lyrics_from_url() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractstaticmethod
|
||||||
|
def strip_punctuation() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractstaticmethod
|
||||||
|
def get_descriptive_statistics() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def reset(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def find_artists(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def sort_artists(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_accuracy_scores(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_top_five_results(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def find_all_albums(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def find_all_tracks(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def find_lyrics_urls(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def find_all_lyrics(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def count_words_in_lyrics(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def calculate_track_totals(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def calculate_final_average_by_album(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def calculate_final_average_by_year(self) -> None:
|
||||||
|
pass
|
||||||
172
src/musicbrainzapi/api/lyrics/director.py
Normal file
172
src/musicbrainzapi/api/lyrics/director.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
from musicbrainzapi.api.lyrics.builder import LyricsBuilder
|
||||||
|
from musicbrainzapi.api.lyrics import Lyrics
|
||||||
|
|
||||||
|
|
||||||
|
class LyricsClickDirector:
|
||||||
|
"""Director for Lyrics builder."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Create a Director to orchestrate the builder."""
|
||||||
|
self._builder = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_product(builder_inst: LyricsBuilder) -> Lyrics:
|
||||||
|
"""Returns the constructed Lyrics object
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
builder_inst : LyricsBuilder
|
||||||
|
Builder class for Lyrics object
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Lyrics
|
||||||
|
Lyrics object
|
||||||
|
"""
|
||||||
|
return builder_inst._product
|
||||||
|
|
||||||
|
@property
|
||||||
|
def builder(self) -> LyricsBuilder:
|
||||||
|
return self._builder
|
||||||
|
|
||||||
|
@builder.setter
|
||||||
|
def builder(self, builder: LyricsBuilder) -> None:
|
||||||
|
self._builder = builder
|
||||||
|
|
||||||
|
def _get_initial_artists(self, artist: str, country: str) -> None:
|
||||||
|
"""Search Musicbrainz api for an artist
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
artist : str
|
||||||
|
Artist to search for
|
||||||
|
country : str
|
||||||
|
Country artist comes from.
|
||||||
|
"""
|
||||||
|
self.builder.artist = artist
|
||||||
|
self.builder.country = country
|
||||||
|
self.builder.set_useragent()
|
||||||
|
self.builder.find_artists()
|
||||||
|
self.builder.sort_artists()
|
||||||
|
self.builder.get_accuracy_scores()
|
||||||
|
self.builder.get_top_five_results()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _confirm_final_artist(self) -> None:
|
||||||
|
"""Confirm the artist from the user.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
SystemExit
|
||||||
|
If no artist is found will cleanly quit.
|
||||||
|
|
||||||
|
"""
|
||||||
|
artist_meta = None
|
||||||
|
for i, j in self.builder._top_five_results.items():
|
||||||
|
artist_meta = 'Multiple' if j <= 100 else None
|
||||||
|
|
||||||
|
if artist_meta == 'Multiple':
|
||||||
|
_position = []
|
||||||
|
click.echo(
|
||||||
|
click.style(
|
||||||
|
f'Musicbrainz found several results for '
|
||||||
|
f'{self.builder.artist[0]}. Which artist/group do you want'
|
||||||
|
'?',
|
||||||
|
fg='green',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for i, j in zip(self.builder._top_five_results, range(1, 6)):
|
||||||
|
click.echo(
|
||||||
|
f'[{j}] {self.builder._sort_names.get(i)}'
|
||||||
|
f' ({self.builder._accuracy_scores.get(i)}% match)'
|
||||||
|
)
|
||||||
|
_position.append(i)
|
||||||
|
chosen = int(
|
||||||
|
click.prompt(
|
||||||
|
click.style(f'Enter choice, default is', blink=True),
|
||||||
|
default=1,
|
||||||
|
type=click.IntRange(
|
||||||
|
1, len(self.builder._top_five_results)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
choice = _position[chosen - 1]
|
||||||
|
click.echo(f'You chose {self.builder._sort_names.get(choice)}')
|
||||||
|
self._artist = self.builder._sort_names.get(choice).split('|')[0]
|
||||||
|
self._artist_id = choice
|
||||||
|
|
||||||
|
# Set artist and artistID on builder + product
|
||||||
|
self.builder.artist_id = self._artist_id
|
||||||
|
self.builder.artist = self._artist
|
||||||
|
|
||||||
|
elif artist_meta is None:
|
||||||
|
click.echo(
|
||||||
|
f'Musicbrainz did not find any results for '
|
||||||
|
f'{self.builder.artist[0]}. Check the spelling or consider '
|
||||||
|
'alternative names that the artist/group may go by.'
|
||||||
|
)
|
||||||
|
raise SystemExit()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _query_for_data(self) -> None:
|
||||||
|
"""Query Musicbrainz api for albums + track data."""
|
||||||
|
self.builder.find_all_albums()
|
||||||
|
self.builder.find_all_tracks()
|
||||||
|
self.builder._product.all_albums_with_tracks = self.builder.all_albums
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _get_lyrics(self) -> None:
|
||||||
|
"""Get Lyrics for each track
|
||||||
|
"""
|
||||||
|
self.builder.find_lyrics_urls()
|
||||||
|
self.builder.find_all_lyrics()
|
||||||
|
self.builder._product.all_albums_with_lyrics = (
|
||||||
|
self.builder.all_albums_lyrics
|
||||||
|
)
|
||||||
|
self.builder.count_words_in_lyrics()
|
||||||
|
# with open(f'{os.getcwd()}/lyrics_count.json', 'w+') as file:
|
||||||
|
# json.dump(
|
||||||
|
# self.builder.all_albums_lyrics_count,
|
||||||
|
# file,
|
||||||
|
# indent=2,
|
||||||
|
# sort_keys=True,
|
||||||
|
# )
|
||||||
|
self.builder._product.all_albums_lyrics_count = (
|
||||||
|
self.builder.all_albums_lyrics_count
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _calculate_basic_statistics(self) -> None:
|
||||||
|
"""Calculate a basic average for all tracks.
|
||||||
|
"""
|
||||||
|
self.builder.calculate_track_totals()
|
||||||
|
self.builder._product.all_albums_lyrics_sum = (
|
||||||
|
self.builder.all_albums_lyrics_sum
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _calculate_descriptive_statistics(self) -> None:
|
||||||
|
"""Calculate descriptive statistics for album and/or year.
|
||||||
|
"""
|
||||||
|
self.builder.calculate_final_average_by_album()
|
||||||
|
self.builder.calculate_final_average_by_year()
|
||||||
|
self.builder._product.album_statistics = self.builder.album_statistics
|
||||||
|
self.builder._product.year_statistics = self.builder.year_statistics
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _dev(self) -> None:
|
||||||
|
"""Dev function - used for testing
|
||||||
|
"""
|
||||||
|
self.builder.calculate_final_average_by_album()
|
||||||
|
self.builder.calculate_final_average_by_year()
|
||||||
|
self.builder._product.album_statistics = self.builder.album_statistics
|
||||||
|
self.builder._product.year_statistics = self.builder.year_statistics
|
||||||
|
self.builder._product.artist_id = None
|
||||||
|
self.builder._product.artist = 'Katzenjammer'
|
||||||
|
self.builder._product.show_summary()
|
||||||
|
self.builder._product.show_summary_statistics(group_by='year')
|
||||||
|
return self
|
||||||
@@ -1,62 +1,69 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from musicbrainzapi.__version__ import __version__
|
||||||
|
from musicbrainzapi.__header__ import __header__
|
||||||
|
|
||||||
CONTEXT_SETTINGS = dict(auto_envvar_prefix='COMPLEX')
|
# pylint:disable=invalid-name
|
||||||
|
|
||||||
|
CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX")
|
||||||
|
|
||||||
|
|
||||||
class Environment(object):
|
class Environment:
|
||||||
|
"""Environment class to house shared parameters between all subcommands."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.verbose = False
|
self.verbose = False
|
||||||
self.home = os.getcwd()
|
self.home = os.getcwd()
|
||||||
|
|
||||||
|
|
||||||
pass_environment = click.make_pass_decorator(Environment, ensure=True)
|
pass_environment = click.make_pass_decorator(
|
||||||
|
Environment, ensure=True
|
||||||
|
)
|
||||||
cmd_folder = os.path.abspath(
|
cmd_folder = os.path.abspath(
|
||||||
os.path.join(os.path.dirname(__file__), 'commands')
|
os.path.join(os.path.dirname(__file__), "commands")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ComplexCLI(click.MultiCommand):
|
class ComplexCLI(click.MultiCommand):
|
||||||
|
"""Access and run subcommands."""
|
||||||
|
|
||||||
def list_commands(self, ctx):
|
def list_commands(self, ctx):
|
||||||
rv = []
|
"""List all subcommands."""
|
||||||
for filename in os.listdir(cmd_folder):
|
rv = [
|
||||||
if filename.endswith('.py') and filename.startswith('cmd_'):
|
filename[4:-3]
|
||||||
rv.append(filename[4:-3])
|
for filename in os.listdir(cmd_folder)
|
||||||
|
if filename.endswith(".py") and filename.startswith("cmd_")
|
||||||
|
]
|
||||||
rv.sort()
|
rv.sort()
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def get_command(self, ctx, name):
|
def get_command(self, ctx, cmd_name):
|
||||||
try:
|
"""Get chosen subcummands."""
|
||||||
if sys.version_info[0] == 2:
|
mod = import_module(f"musicbrainzapi.cli.commands.cmd_{cmd_name}")
|
||||||
name = name.encode('ascii', 'replace')
|
return getattr(mod, cmd_name)
|
||||||
mod = import_module(f'musicbrainzapi.cli.commands.cmd_{name}')
|
|
||||||
# mod = __import__(
|
|
||||||
# 'complex.commands.cmd_' + name, None, None, ['cli']
|
|
||||||
# )
|
|
||||||
except ImportError as e:
|
|
||||||
print(e)
|
|
||||||
return
|
|
||||||
return mod.cli
|
|
||||||
|
|
||||||
|
|
||||||
@click.command(cls=ComplexCLI, context_settings=CONTEXT_SETTINGS)
|
@click.command(cls=ComplexCLI, context_settings=CONTEXT_SETTINGS)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--home',
|
"-p",
|
||||||
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
"--path",
|
||||||
help='Changes the folder to operate on.',
|
type=click.Path(exists=True, file_okay=False, resolve_path=True, writable=True),
|
||||||
|
help="Local path to save any output files.",
|
||||||
|
default=os.getcwd(),
|
||||||
|
)
|
||||||
|
@click.option("-v", "--verbose", is_flag=True, help="Enables verbose mode.")
|
||||||
|
@click.version_option(
|
||||||
|
version=__version__,
|
||||||
|
prog_name=__header__,
|
||||||
|
message=f"{__header__} version {__version__} 🎤",
|
||||||
)
|
)
|
||||||
@click.option('-v', '--verbose', is_flag=True, help='Enables verbose mode.')
|
|
||||||
@pass_environment
|
@pass_environment
|
||||||
def cli(ctx, verbose, home):
|
def cli(ctx, verbose, path):
|
||||||
"""A complex command line interface."""
|
"""Display base command for the musicbrainzapi program."""
|
||||||
ctx.verbose = verbose
|
ctx.verbose = verbose
|
||||||
if home is not None:
|
if path is not None:
|
||||||
ctx.home = home
|
click.echo(f"Path set to {os.path.expanduser(path)}")
|
||||||
|
ctx.path = os.path.expanduser(path)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
cli()
|
|
||||||
|
|||||||
@@ -1,81 +1,39 @@
|
|||||||
from pprint import pprint
|
import json
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import musicbrainzngs
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
from musicbrainzapi.cli.cli import pass_environment
|
from musicbrainzapi.cli.cli import pass_environment
|
||||||
from musicbrainzapi.api import authenticate
|
|
||||||
from musicbrainzapi.api.command_builders import lyrics
|
import musicbrainzapi.wordcloud
|
||||||
|
from musicbrainzapi.api.lyrics.builder import LyricsBuilder
|
||||||
|
from musicbrainzapi.api.lyrics.director import LyricsClickDirector
|
||||||
|
|
||||||
|
|
||||||
class LyricsInfo:
|
@click.option('--dev', is_flag=True, help='Development flag. Do not use.')
|
||||||
"""docstring for LyricsInfo"""
|
@click.option(
|
||||||
|
'--save-output',
|
||||||
def __init__(self, artist: str, country: str = None) -> None:
|
required=False,
|
||||||
authenticate.set_useragent()
|
help='Save the output to json files locally. Will use the path parameter'
|
||||||
self.artist = artist
|
' if provided else defaults to current working directory.',
|
||||||
self.country = country
|
is_flag=True,
|
||||||
super().__init__()
|
default=False,
|
||||||
|
|
||||||
def _search_artist(self) -> None:
|
|
||||||
self.artists = musicbrainzngs.search_artists(
|
|
||||||
artist=self.artist, country=self.country
|
|
||||||
)
|
)
|
||||||
# pprint(self.artists['artist-list'])
|
@click.option(
|
||||||
|
'--wordcloud',
|
||||||
if self.artists.get('artist-count') == 0:
|
required=False,
|
||||||
self.chosen_artist = 'Null'
|
help='Generates a wordcloud from lyrics.',
|
||||||
|
is_flag=True,
|
||||||
# Get all results
|
default=False,
|
||||||
|
|
||||||
self.sort_names = dict(
|
|
||||||
(i.get('id'), f'{i.get("sort-name")} | {i.get("disambiguation")}')
|
|
||||||
if i.get('disambiguation') is not None
|
|
||||||
else (i.get('id'), f'{i.get("sort-name")}')
|
|
||||||
for i in self.artists['artist-list']
|
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
# Get accuracy scores
|
'--show-summary',
|
||||||
|
required=False,
|
||||||
self._accuracy_scores = dict(
|
help='Show summary statistics for the artist.',
|
||||||
(i.get('id'), int(i.get('ext:score', '0')))
|
type=click.Choice(['album', 'year', 'all']),
|
||||||
for i in self.artists['artist-list']
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# pprint(self._accuracy_scores)
|
|
||||||
|
|
||||||
# Get top 5 results
|
|
||||||
|
|
||||||
self.top_five_results = dict(
|
|
||||||
(i, self._accuracy_scores.get(i))
|
|
||||||
for i in sorted(
|
|
||||||
self._accuracy_scores,
|
|
||||||
key=self._accuracy_scores.get,
|
|
||||||
reverse=True,
|
|
||||||
)[0:5]
|
|
||||||
)
|
|
||||||
|
|
||||||
# pprint(self.top_five_results)
|
|
||||||
|
|
||||||
# Check for 100% match
|
|
||||||
self.chosen_artist = None
|
|
||||||
for i, j in self.top_five_results.items():
|
|
||||||
self.chosen_artist = 'Multiple' if j <= 100 else None
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class CommandUtility:
|
|
||||||
"""docstring for CommandUtility"""
|
|
||||||
|
|
||||||
def get_multiple_options(option: tuple):
|
|
||||||
for i in option:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# @click.argument('path', required=False, type=click.Path(resolve_path=True))
|
|
||||||
# @click.command(short_help='a test command')
|
|
||||||
@click.option(
|
@click.option(
|
||||||
'--country',
|
'--country',
|
||||||
'-c',
|
'-c',
|
||||||
@@ -83,7 +41,7 @@ class CommandUtility:
|
|||||||
required=False,
|
required=False,
|
||||||
multiple=False,
|
multiple=False,
|
||||||
type=str,
|
type=str,
|
||||||
help='ISO A-2 Country code (https://en.wikipedia.org/wiki/ISO_3166-1_alpha'
|
help='Filter artist by country. This is optional but can narrow down a search if many artists share the same or similar names. Country must be a ISO A-2 Country code (https://en.wikipedia.org/wiki/ISO_3166-1_alpha'
|
||||||
'-2) Example: GB',
|
'-2) Example: GB',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
@@ -92,26 +50,73 @@ class CommandUtility:
|
|||||||
required=True,
|
required=True,
|
||||||
multiple=True,
|
multiple=True,
|
||||||
type=str,
|
type=str,
|
||||||
help='Artist/Group to search lyrics for.',
|
help='Artist/Group to search.',
|
||||||
)
|
)
|
||||||
@click.command()
|
@click.command()
|
||||||
@pass_environment
|
@pass_environment
|
||||||
def cli(ctx, artist: str, country: Union[str, None]) -> None:
|
def lyrics(
|
||||||
|
ctx,
|
||||||
|
artist: str,
|
||||||
|
country: Union[str, None],
|
||||||
|
dev: bool,
|
||||||
|
show_summary: str,
|
||||||
|
wordcloud: bool,
|
||||||
|
save_output: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Search for lyrics statistics of an Artist/Group. Optionally save the results to disk, and show a wordcloud. Descriptive statistics can be shown in addition to the final average.
|
||||||
"""
|
"""
|
||||||
Search for lyrics of an Artist/Group.
|
director = LyricsClickDirector()
|
||||||
"""
|
builder = LyricsBuilder()
|
||||||
# print(artist)
|
|
||||||
director = lyrics.LyricsClickDirector()
|
|
||||||
builder = lyrics.LyricsBuilder()
|
|
||||||
director.builder = builder
|
director.builder = builder
|
||||||
|
if dev:
|
||||||
|
director._dev()
|
||||||
|
raise (SystemExit)
|
||||||
|
|
||||||
|
# build the Lyrics object
|
||||||
director._get_initial_artists(artist, country)
|
director._get_initial_artists(artist, country)
|
||||||
director._confirm_final_artist()
|
director._confirm_final_artist()
|
||||||
director._set_artist_id_on_product()
|
director._query_for_data()
|
||||||
director._search_for_all_tracks()
|
director._get_lyrics()
|
||||||
# builder.do_filter_albums_official()
|
director._calculate_basic_statistics()
|
||||||
# builder.do_search_album_tracks()
|
director._calculate_descriptive_statistics()
|
||||||
|
|
||||||
|
# Get the Lyrics object
|
||||||
|
lyrics_0 = director.builder.product
|
||||||
|
# lyrics_obj.append(lyrics_0)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
# Show basic count
|
||||||
# LyricsInfo('Queenifie')._search_artist()
|
lyrics_0.show_summary()
|
||||||
LyricsInfo('Que')._search_artist()
|
|
||||||
|
# Show summary statistics
|
||||||
|
if show_summary == 'all':
|
||||||
|
lyrics_0.show_summary_statistics(group_by='album')
|
||||||
|
lyrics_0.show_summary_statistics(group_by='year')
|
||||||
|
elif show_summary in ['album', 'year']:
|
||||||
|
lyrics_0.show_summary_statistics(group_by=show_summary)
|
||||||
|
|
||||||
|
# Show wordcloud
|
||||||
|
if wordcloud:
|
||||||
|
click.echo('Generating wordcloud')
|
||||||
|
cloud = musicbrainzapi.wordcloud.LyricsWordcloud.use_microphone(
|
||||||
|
lyrics_0.all_albums_lyrics_count
|
||||||
|
)
|
||||||
|
cloud.create_word_cloud()
|
||||||
|
show = click.confirm(
|
||||||
|
'Wordcloud ready - press enter to show.', default=True
|
||||||
|
)
|
||||||
|
plt.imshow(
|
||||||
|
cloud.wc.recolor(
|
||||||
|
color_func=cloud.generate_grey_colours, random_state=3
|
||||||
|
),
|
||||||
|
interpolation='bilinear',
|
||||||
|
)
|
||||||
|
plt.axis('off')
|
||||||
|
if show:
|
||||||
|
plt.show()
|
||||||
|
if save_output:
|
||||||
|
click.echo(f'Saving output to {ctx.path}')
|
||||||
|
path = ctx.path if ctx.path[-1] == '/' else ctx.path + '/'
|
||||||
|
attr = lyrics_0._attributes
|
||||||
|
for a in attr:
|
||||||
|
with open(f'{path}{a}.json', 'w') as f:
|
||||||
|
json.dump(getattr(lyrics_0, a), f, indent=2)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
BIN
src/musicbrainzapi/wordcloud/.DS_Store
vendored
Normal file
BIN
src/musicbrainzapi/wordcloud/.DS_Store
vendored
Normal file
Binary file not shown.
165
src/musicbrainzapi/wordcloud/__init__.py
Normal file
165
src/musicbrainzapi/wordcloud/__init__.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
"""
|
||||||
|
Wordcloud from lyrics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import collections
|
||||||
|
from importlib import resources
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
from PIL import Image
|
||||||
|
from wordcloud import STOPWORDS, WordCloud
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from musicbrainzapi.api.lyrics import Lyrics
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
import PIL.PngImagePlugin.PngImageFile
|
||||||
|
|
||||||
|
|
||||||
|
class LyricsWordcloud:
|
||||||
|
"""Create a word cloud from Lyrics.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
all_albums_lyrics_count : list
|
||||||
|
List of all albums + track lyrics counted by each word
|
||||||
|
char_mask : np.array
|
||||||
|
numpy array containing data for the word cloud image
|
||||||
|
freq : collections.Counter
|
||||||
|
Counter object containing counts for all words across all tracks
|
||||||
|
lyrics_list : list
|
||||||
|
List of all words from all lyrics across all tracks.
|
||||||
|
pillow_img : PIL.PngImagePlugin.PngImageFile
|
||||||
|
pillow image of the word cloud base
|
||||||
|
wc : wordcloud.WordCloud
|
||||||
|
WordCloud object
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pillow_img: 'PIL.PngImagePlugin.PngImageFile',
|
||||||
|
all_albums_lyrics_count: 'Lyrics.all_albums_lyrics_count',
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a worcloud object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
pillow_img : PIL.PngImagePlugin.PngImageFile
|
||||||
|
pillow image of the word cloud base
|
||||||
|
all_albums_lyrics_count : Lyrics.all_albums_lyrics_count
|
||||||
|
List of all albums + track lyrics counted by each word
|
||||||
|
"""
|
||||||
|
self.pillow_img = pillow_img
|
||||||
|
self.all_albums_lyrics_count = all_albums_lyrics_count
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def use_microphone(
|
||||||
|
cls, all_albums_lyrics_count: 'Lyrics.all_albums_lyrics_count',
|
||||||
|
) -> LyricsWordcloud:
|
||||||
|
"""
|
||||||
|
Class method to instantiate with a microphone base image.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
all_albums_lyrics_count : Lyrics.all_albums_lyrics_count
|
||||||
|
List of all albums + track lyrics counted by each word
|
||||||
|
|
||||||
|
"""
|
||||||
|
mic_resource = resources.path(
|
||||||
|
'musicbrainzapi.wordcloud.resources', 'mic.png'
|
||||||
|
)
|
||||||
|
with mic_resource as m:
|
||||||
|
mic_img = Image.open(m)
|
||||||
|
|
||||||
|
return cls(mic_img, all_albums_lyrics_count)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_grey_colours(
|
||||||
|
# word: str,
|
||||||
|
# font_size: str,
|
||||||
|
# random_state: typing.Union[None, bool] = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> str:
|
||||||
|
"""Static method to generate a random grey colour."""
|
||||||
|
colour = f'hsl(0, 0%, {random.randint(60, 100)}%)'
|
||||||
|
return colour
|
||||||
|
|
||||||
|
def _get_lyrics_list(self) -> None:
|
||||||
|
"""Gets all words from lyrics in a single list + cleans them.
|
||||||
|
"""
|
||||||
|
self.lyrics_list = list()
|
||||||
|
for i in self.all_albums_lyrics_count:
|
||||||
|
for _, lyric in i.items():
|
||||||
|
for track in lyric:
|
||||||
|
try:
|
||||||
|
for word in track:
|
||||||
|
for _ in range(1, word[1]):
|
||||||
|
cleaned = word[0]
|
||||||
|
cleaned = re.sub(
|
||||||
|
r'[\(\[].*?[\)\]]', ' ', cleaned
|
||||||
|
)
|
||||||
|
cleaned = re.sub(
|
||||||
|
r'[^a-zA-Z0-9\s]', '', cleaned
|
||||||
|
)
|
||||||
|
cleaned = cleaned.lower()
|
||||||
|
if cleaned in STOPWORDS:
|
||||||
|
continue
|
||||||
|
self.lyrics_list.append(cleaned)
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _get_frequencies(self) -> None:
|
||||||
|
"""Get frequencies of words from a list.
|
||||||
|
"""
|
||||||
|
self.freq = collections.Counter(self.lyrics_list)
|
||||||
|
|
||||||
|
def _get_char_mask(self) -> None:
|
||||||
|
"""Gets a numpy array for the image file.
|
||||||
|
"""
|
||||||
|
self.char_mask = np.array(self.pillow_img)
|
||||||
|
|
||||||
|
def _generate_word_cloud(self) -> None:
|
||||||
|
"""Generates a word cloud
|
||||||
|
"""
|
||||||
|
self.wc = WordCloud(
|
||||||
|
max_words=150,
|
||||||
|
width=1500,
|
||||||
|
height=1500,
|
||||||
|
mask=self.char_mask,
|
||||||
|
random_state=1,
|
||||||
|
).generate_from_frequencies(self.freq)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _generate_plot(self) -> None:
|
||||||
|
"""Plots the wordcloud and sets matplotlib options.
|
||||||
|
"""
|
||||||
|
plt.imshow(
|
||||||
|
self.wc.recolor(
|
||||||
|
color_func=self.generate_grey_colours, random_state=3
|
||||||
|
),
|
||||||
|
interpolation='bilinear',
|
||||||
|
)
|
||||||
|
plt.axis('off')
|
||||||
|
return self
|
||||||
|
|
||||||
|
# def show_word_cloud(self):
|
||||||
|
# """Shows the word cloud.
|
||||||
|
# """
|
||||||
|
# plt.show()
|
||||||
|
|
||||||
|
def create_word_cloud(self) -> None:
|
||||||
|
"""Creates a word cloud
|
||||||
|
"""
|
||||||
|
self._get_lyrics_list()
|
||||||
|
self._get_frequencies()
|
||||||
|
self._get_char_mask()
|
||||||
|
self._generate_word_cloud()
|
||||||
|
self._generate_plot()
|
||||||
|
return self
|
||||||
BIN
src/musicbrainzapi/wordcloud/resources/.DS_Store
vendored
Normal file
BIN
src/musicbrainzapi/wordcloud/resources/.DS_Store
vendored
Normal file
Binary file not shown.
0
src/musicbrainzapi/wordcloud/resources/__init__.py
Normal file
0
src/musicbrainzapi/wordcloud/resources/__init__.py
Normal file
BIN
src/musicbrainzapi/wordcloud/resources/mic.png
Normal file
BIN
src/musicbrainzapi/wordcloud/resources/mic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
@@ -1,5 +0,0 @@
|
|||||||
from musicbrainzapi import __version__
|
|
||||||
|
|
||||||
|
|
||||||
def test_version():
|
|
||||||
assert __version__ == '0.1.0'
|
|
||||||
Reference in New Issue
Block a user