79 Commits

Author SHA1 Message Date
cd8117343b Merge branch 'develop' 2020-03-09 12:26:00 +00:00
8dc88f6361 updating poetry installation instructions 2020-03-09 12:25:45 +00:00
306eb82237 Merge branch 'develop' 2020-03-09 12:22:43 +00:00
0a77fa34fd adding poetry to installation instructions 2020-03-09 12:22:35 +00:00
5aefcc2a2d Merge branch 'develop' 2020-03-09 12:12:28 +00:00
0034340d63 updating comments document 2020-03-09 12:12:11 +00:00
02cb79c4b2 Merge branch 'master' into develop 2020-03-09 11:57:22 +00:00
26b346d359 Merge branch 'documentation' 2020-03-09 11:56:19 +00:00
1dae95735f updating documentation 2020-03-09 11:56:03 +00:00
78544673b4 Merge branch 'develop' 2020-03-09 11:38:49 +00:00
e8ce4b59f8 Merge branch 'documentation' into develop 2020-03-09 11:38:36 +00:00
e09728a7c7 updating documentation 2020-03-09 11:38:22 +00:00
ae6c2bf985 Merge branch 'master' into documentation 2020-03-09 03:38:06 +00:00
fd144abff0 Merge branch 'develop' 2020-03-09 03:37:17 +00:00
d499ee175e removing badge 2020-03-09 03:37:11 +00:00
fadcb98d81 Merge branch 'develop' 2020-03-09 03:34:48 +00:00
aa98102d6a removing badge 2020-03-09 03:34:36 +00:00
a10426f043 Merge branch 'develop' 2020-03-09 03:31:09 +00:00
78ac63ca36 code quality improvements 2020-03-09 03:30:52 +00:00
2d8f8dc63f Merge branch 'master' into develop 2020-03-09 03:20:26 +00:00
62413ce829 Merge branch 'documentation' 2020-03-09 03:10:02 +00:00
52e1d3f8db updating documentation 2020-03-09 03:09:50 +00:00
06368aa532 updating documentation 2020-03-09 02:43:35 +00:00
9d59a8f4fb Merge branch 'develop' into documentation 2020-03-09 02:00:34 +00:00
237be15ecd updating readme and adding coverage 2020-03-09 01:59:38 +00:00
bc4d939eff Merge branch 'develop' 2020-03-09 01:47:24 +00:00
ed9a30db13 code quality fixes 2020-03-09 01:47:16 +00:00
10a46516d5 Merge branch 'develop' 2020-03-09 01:31:16 +00:00
15595892ec updating docstring 2020-03-09 01:30:50 +00:00
13188c38b9 Merge branch 'documentation' into develop 2020-03-09 01:15:31 +00:00
8fcb0d8704 updating latest documentation 2020-03-09 01:15:20 +00:00
3fecf9d723 updating toctree 2020-03-09 00:22:02 +00:00
76a5d580ab updating toc tree 2020-03-09 00:11:55 +00:00
a322df759e updating documentation 2020-03-08 23:38:36 +00:00
f06c9f43d4 adding api documentation 2020-03-08 23:27:42 +00:00
199302afb3 modules update 2020-03-08 23:19:14 +00:00
d6e7e49f3f latest documentation 2020-03-08 23:19:07 +00:00
c711795cce adding CLI to toctree 2020-03-08 22:20:18 +00:00
026bfc12b7 adding CLI 2020-03-08 22:19:55 +00:00
06a42afb2f adding custom css 2020-03-08 21:49:16 +00:00
4d5b32bcd9 updating badges 2020-03-08 21:35:34 +00:00
509cd86830 updating README and CHANGELOG 2020-03-08 21:32:44 +00:00
f2145d8cdc updating documentation 2020-03-08 15:45:23 +00:00
d89c738c97 updating documentation 2020-03-08 14:35:24 +00:00
263cf33cf6 updating documentation 2020-03-08 02:50:32 +00:00
62111cfe63 updating documentation & dockerfile 2020-03-08 02:43:38 +00:00
5a1e4e72a1 updating setup.py 2020-03-08 02:08:49 +00:00
74c18b50b9 Merge branch 'develop' 2020-03-08 02:04:13 +00:00
0d64bf7b83 updating docstrings 2020-03-08 02:03:21 +00:00
c9632c129a tidying up 2020-03-08 02:03:11 +00:00
1f16f3b433 adding Dockerfile 2020-03-08 02:03:01 +00:00
ff155f81f4 updating badges 2020-03-08 01:30:32 +00:00
383aa2b77b updating documentation 2020-03-08 01:22:06 +00:00
a431ce60b7 updating docstrings 2020-03-07 23:09:25 +00:00
4f7b829c96 adding requirements-dev.txt 2020-03-07 23:00:41 +00:00
ac0a1f6709 adding requirements.txt & setup.py 2020-03-07 22:58:38 +00:00
7214a43421 updating docstrings 2020-03-07 22:53:45 +00:00
1af282be47 adding docs 2020-03-07 22:36:49 +00:00
d3fc96cfdd adding docs 2020-03-07 22:36:33 +00:00
a73a34f1a7 adding requirements and setup.py 2020-03-07 20:47:29 +00:00
bc0b41fae3 Merge branch 'develop' 2020-03-07 20:41:49 +00:00
3ec71d7076 updating to version 1.0.0 2020-03-07 20:41:39 +00:00
805067521f Merge branch 'rework' into develop 2020-03-07 20:37:51 +00:00
7900da7299 adding latest working version 2020-03-07 20:36:42 +00:00
3d1aeaed3c adding latest working version 2020-03-07 16:59:11 +00:00
eed16b9128 adding latest working version 2020-03-07 03:01:24 +00:00
76fe7333db adding latest working version 2020-03-07 02:40:05 +00:00
6456e3645e Merge branch 'rework' into develop 2020-03-07 01:24:36 +00:00
19e706eb50 adding latest working version 2020-03-07 01:24:24 +00:00
dtomlinson
94476113da adding latest working version 2020-03-06 18:42:29 +00:00
dtomlinson
2d792a4a50 adding latest working version * 2020-03-06 18:29:10 +00:00
dtomlinson
c0841a0035 adding latest working version 2020-03-06 18:20:56 +00:00
dtomlinson
2af1cc68f3 adding latest working version 2020-03-06 18:05:12 +00:00
dtomlinson
6a3c09ca21 adding latest working version 2020-03-06 17:24:14 +00:00
be8ce7edcb latest working version 2020-03-06 01:49:29 +00:00
d2b43414a4 latest working version 2020-03-06 00:14:24 +00:00
50e9ce6e2e latest working version 2020-03-05 21:31:06 +00:00
dtomlinson
36686c668d adding latest rework 2020-03-05 18:50:21 +00:00
dtomlinson
14da065144 adding latest before re-work 2020-03-05 17:38:03 +00:00
44 changed files with 2610 additions and 4575 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

21
CHANGELOG.rst Normal file
View 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
View 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

View File

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

View File

@@ -0,0 +1,5 @@
@import url("css/theme.css");
.highlight {
background: white !important
}

View File

@@ -0,0 +1 @@
.. include:: ../../CHANGELOG.rst

126
docs/source/comments.rst Normal file
View 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 AllAmerican 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
View 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
View File

@@ -0,0 +1,5 @@
.. role:: modname
:class: modname
.. role:: title
:class: title

30
docs/source/index.rst Normal file
View 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`

View File

@@ -0,0 +1 @@
.. include:: ../../README.rst

View File

@@ -0,0 +1,7 @@
musicbrainzapi
--------------
.. toctree::
:maxdepth: 3
musicbrainzapi

View 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:

View 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:

View File

@@ -0,0 +1,19 @@
musicbrainzapi
===============
.. automodule:: musicbrainzapi
:members:
:undoc-members:
:show-inheritance:
:private-members:
Subpackages
-----------
.. toctree::
:maxdepth: 1
musicbrainzapi.api
musicbrainzapi.wordcloud

View File

@@ -0,0 +1,9 @@
********************************
musicbrainzapi.wordcloud package
********************************
.. automodule:: musicbrainzapi.wordcloud
:members:
:undoc-members:
:show-inheritance:
:private-members:

578
poetry.lock generated
View File

@@ -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"},

View File

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

Binary file not shown.

BIN
src/musicbrainzapi/.DS_Store vendored Normal file

Binary file not shown.

View File

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

View File

@@ -1 +1 @@
__version__ = '0.1.0' __version__ = '1.0.0'

BIN
src/musicbrainzapi/api/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1 +0,0 @@
from . import lyrics

View File

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

View 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

View 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))
)

View 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

View 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

View File

@@ -1,9 +1,10 @@
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') CONTEXT_SETTINGS = dict(auto_envvar_prefix='COMPLEX')
@@ -29,34 +30,31 @@ class ComplexCLI(click.MultiCommand):
rv.sort() rv.sort()
return rv return rv
def get_command(self, ctx, name): def get_command(self, ctx, cmd_name):
try: mod = import_module(f'musicbrainzapi.cli.commands.cmd_{cmd_name}')
if sys.version_info[0] == 2: return getattr(mod, cmd_name)
name = name.encode('ascii', 'replace')
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, path):
"""A complex command line interface.""" """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()

View File

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

Binary file not shown.

View 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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

View File

@@ -1,5 +0,0 @@
from musicbrainzapi import __version__
def test_version():
assert __version__ == '0.1.0'