diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 39d9975..c6bc090 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -28,6 +28,7 @@ test-3.5: test-2.7: <<: *test_definition + allow_failure: true variables: PYTHON_VERSION: "2.7" diff --git a/.makefiles/python.mk b/.makefiles/python.mk index 23919a4..1b8d7b8 100644 --- a/.makefiles/python.mk +++ b/.makefiles/python.mk @@ -29,7 +29,7 @@ build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions docker tag $(IMAGEWTAG)-python$(PYMAIN) $(IMAGEWTAG) build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7) - docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .; + docker build --pull -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .; dev-%: ## Launch a specific development environment using docker (e.g. dev-2.7) @docker start $(NAME)-dev$* || (\ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..406dd55 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +* Objects can control the keys that will be used in `serialize`/`jsonld`/`as_dict` by specifying a list of keys in `terse_keys`. +e.g. +```python +>>> class MyModel(senpy.models.BaseModel): +... _terse_keys = ['visible'] +... invisible = 5 +... visible = 1 +... +>>> m = MyModel(id='testing') +>>> m.jsonld() +{'invisible': 5, 'visible': 1, '@id': 'testing'} +>>> m.jsonld(verbose=False) +{'visible': 1} +``` +* Configurable logging format. +* Added default terse keys for the most common classes (entry, sentiment, emotion...). +* Flag parameters (boolean) are set to true even when no value is added (e.g. `&verbose` is the same as `&verbose=true`). +* Plugin and parameter descriptions are now formatted with (showdown)[https://github.com/showdownjs/showdown]. +* The web UI requests extra_parameters from the server. This is useful for pipelines. See #52 +* First batch of semantic tests (using SPARQL) + +### Changed +* `install_deps` now checks what requirements are already met before installing with pip. +* Help is now provided verbosely by default +* Other outputs are terse by default. This means some properties are now hidden unless verbose is set. +* `sentiments` and `emotions` are now `marl:hasOpinion` and `onyx:hasEmotionSet`, respectively. +* Nicer logging format +* Context aliases (e.g. `sentiments` and `emotions` properties) have been replaced with the original properties (e.g. `marl:hasOpinion` and `onyx:hasEmotionSet**), to use aliases, pass the `aliases** parameter. +* Several UI improvements + * Dedicated tab to show the list of plugins + * URLs in plugin descriptions are shown as links + * The format of the response is selected by clicking on a tab instead of selecting from a drop-down + * list of examples + * Bootstrap v4 +* RandEmotion and RandSentiment are no longer included in the base set of plugins +* The `--plugin-folder` option can be used more than once, and every folder will be added to the app. + +### Deprecated +### Removed +* Python 2.7 is no longer test or officially supported +### Fixed +* Plugin descriptions are now dedented when they are extracted from the docstring. +### Security + diff --git a/Makefile b/Makefile index 9af7a68..8483d2a 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ IMAGENAME=gsiupm/senpy # The first version is the main one (used for quick builds) # See .makefiles/python.mk for more info -PYVERSIONS=3.5 2.7 +PYVERSIONS=3.6 3.7 DEVPORT=5000 diff --git a/Procfile b/Procfile index 8fcdd4f..20c618b 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: python -m senpy --host 0.0.0.0 --port $PORT --default-plugins +web: python -m senpy --host 0.0.0.0 --port $PORT diff --git a/README.rst b/README.rst index 631671e..cef05ad 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,19 @@ .. image:: img/header.png :width: 100% - :target: http://demos.gsi.dit.upm.es/senpy + :target: http://senpy.gsi.upm.es .. image:: https://travis-ci.org/gsi-upm/senpy.svg?branch=master :target: https://travis-ci.org/gsi-upm/senpy +.. image:: https://lab.gsi.upm.es/senpy/senpy/badges/master/pipeline.svg + :target: https://lab.gsi.upm.es/senpy/senpy/commits/master + +.. image:: https://lab.gsi.upm.es/senpy/senpy/badges/master/coverage.svg + :target: https://lab.gsi.upm.es/senpy/senpy/commits/master + +.. image:: https://img.shields.io/pypi/l/requests.svg + :target: https://lab.gsi.upm.es/senpy/senpy/ + Senpy lets you create sentiment analysis web services easily, fast and using a well known API. As a bonus, senpy services use semantic vocabularies (e.g. `NIF `_, `Marl `_, `Onyx `_) and formats (turtle, JSON-LD, xml-rdf). @@ -12,7 +21,7 @@ Have you ever wanted to turn your sentiment analysis algorithms into a service? With senpy, now you can. It provides all the tools so you just have to worry about improving your algorithms: -`See it in action. `_ +`See it in action. `_ Installation ------------ @@ -38,9 +47,9 @@ If you want to install senpy globally, use sudo instead of the ``--user`` flag. Docker Image ************ -Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 gsiupm/senpy --default-plugins``. +Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 gsiupm/senpy``. -To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v :/plugins gsiupm/senpy --default-plugins -f /plugins`` +To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v :/plugins gsiupm/senpy -f /plugins`` Developing @@ -125,6 +134,16 @@ For more information, check out the `documentation ' where is one of" @echo " html to make standalone HTML files" + @echo " entr to watch for changes and continuously make HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @@ -49,6 +50,9 @@ help: clean: rm -rf $(BUILDDIR)/* +entr: + while true; do ag -g rst | entr -d make html; done + html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo diff --git a/docs/Quickstart.ipynb b/docs/Quickstart.ipynb new file mode 100644 index 0000000..b701561 --- /dev/null +++ b/docs/Quickstart.ipynb @@ -0,0 +1,4659 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Consuming Senpy services" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This short tutorial will teach you how to consume services in several ways, taking advantage of the features of the framework.\n", + "\n", + "In particular, we will cover:\n", + "\n", + "* Annotating text with sentiment\n", + "* Annotating text with emotion\n", + "* Getting results in different formats (Turtle, XML, text...)\n", + "* Asking for specific emotion models (automatic model conversion)\n", + "* Listing available services in an endpoint\n", + "* Switching to different services\n", + "* Calling multiple services in the same request (Pipelines)\n", + "\n", + "The latest version of this IPython notebook is available at: https://github.com/gsi-upm/senpy/tree/master/docs/Quickstart.ipynb" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Requirements" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the sake of simplicity, this tutorial will use the demo server: http://senpy.gsi.upm.es:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "endpoint = 'http://senpy.gsi.upm.es/api'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This server runs some open source plugins for sentiment and emotion analysis." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The HTTP API of Senpy can be queried with your favourite tool.\n", + "This is just an example using curl:\n", + "\n", + "```bash\n", + "curl \"http://senpy.gsi.upm.es/api/sentiment140\" --data-urlencode \"input=Senpy is awesome\"\n", + "```\n", + "\n", + "For simplicity, in this tutorial we will use the requests library. We will also add a function to add syntax highlighting for the JSON-LD/Turtle results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " from IPython.display import Code\n", + " def pretty(txt, language='json-ld'):\n", + " return Code(txt, language=language)\n", + "except ImportError:\n", + " def pretty(txt, **kwargs):\n", + " print(txt)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once you're familiar with Senpy, you can deploy your own instance quite easily. e.g. using docker:\n", + "\n", + "```\n", + "docker run -ti --name 'SenpyEndpoint' -d -p 5000:5000 gsiupm/senpy\n", + "```\n", + "\n", + "Then, feel free to change the endpoint variable to run the examples in your own instance." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sentiment Analysis of Text" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To start, let us analyse the sentiment in the following sentence: *senpy is a wonderful service*.\n", + "\n", + "For now, we will use the [sentiment140](http://www.sentiment140.com/) service, through the sentiment140 plugin.\n", + "We will later cover how to use a different service.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lIw%3D%3D",\n",
+       "  "@type": "Results",\n",
+       "  "entries": [\n",
+       "    {\n",
+       "      "@id": "prefix:",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [\n",
+       "        {\n",
+       "          "@type": "Sentiment",\n",
+       "          "marl:hasPolarity": "marl:Positive",\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364667.7955277"\n",
+       "        }\n",
+       "      ],\n",
+       "      "nif:isString": "Senpy is awesome",\n",
+       "      "onyx:hasEmotionSet": []\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lIw\\PYZpc{}3D\\PYZpc{}3D\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Results\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}entries\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Sentiment\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasPolarity\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}marl:Positive\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364667.7955277\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is awesome\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lIw%3D%3D\",\n", + " \"@type\": \"Results\",\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [\n", + " {\n", + " \"@type\": \"Sentiment\",\n", + " \"marl:hasPolarity\": \"marl:Positive\",\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364667.7955277\"\n", + " }\n", + " ],\n", + " \"nif:isString\": \"Senpy is awesome\",\n", + " \"onyx:hasEmotionSet\": []\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import requests\n", + "res = requests.get(f'{endpoint}/sentiment140',\n", + " params={\"input\": \"Senpy is awesome\",})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Senpy services always return an object of type `senpy:Results`, with a list of entries.\n", + "You can think of an entry as a self-contained textual context (`nif:Context` and `senpy:Entry`).\n", + "Entries can be as short as a sentence, or as long as a news article.\n", + "\n", + "Each entry has a `nif:isString` property that contains the original text of the entry, and several other properties that are provided by the plugins.\n", + "\n", + "For instance, sentiment annotations are provided through `marl:hasOpinion`.\n", + "\n", + "The annotations are semantic.\n", + "We can ask Senpy for the expanded JSON-LD output to reveal the full URIs of each property and entity:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lJmV4cGFuZGVkPVRydWUj",\n",
+       "  "@type": [\n",
+       "    "http://www.gsi.upm.es/onto/senpy/ns#Results"\n",
+       "  ],\n",
+       "  "http://www.w3.org/ns/prov#used": [\n",
+       "    {\n",
+       "      "@id": "http://senpy.invalid/",\n",
+       "      "@type": [\n",
+       "        "http://www.gsi.upm.es/onto/senpy/ns#Entry"\n",
+       "      ],\n",
+       "      "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#isString": [\n",
+       "        {\n",
+       "          "@value": "Senpy is awesome"\n",
+       "        }\n",
+       "      ],\n",
+       "      "http://www.gsi.dit.upm.es/ontologies/marl/ns#hasOpinion": [\n",
+       "        {\n",
+       "          "@type": [\n",
+       "            "http://www.gsi.upm.es/onto/senpy/ns#Sentiment"\n",
+       "          ],\n",
+       "          "http://www.gsi.dit.upm.es/ontologies/marl/ns#hasPolarity": [\n",
+       "            {\n",
+       "              "@value": "marl:Positive"\n",
+       "            }\n",
+       "          ],\n",
+       "          "http://www.w3.org/ns/prov#wasGeneratedBy": [\n",
+       "            {\n",
+       "              "@id": "http://senpy.invalid/Analysis_1554364668.1011338"\n",
+       "            }\n",
+       "          ]\n",
+       "        }\n",
+       "      ],\n",
+       "      "http://www.gsi.dit.upm.es/ontologies/onyx/ns#hasEmotionSet": []\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lJmV4cGFuZGVkPVRydWUj\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}http://www.gsi.upm.es/onto/senpy/ns\\PYZsh{}Results\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.w3.org/ns/prov\\PYZsh{}used\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.invalid/\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}http://www.gsi.upm.es/onto/senpy/ns\\PYZsh{}Entry\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://persistence.uni\\PYZhy{}leipzig.org/nlp2rdf/ontologies/nif\\PYZhy{}core\\PYZsh{}isString\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is awesome\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/marl/ns\\PYZsh{}hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}http://www.gsi.upm.es/onto/senpy/ns\\PYZsh{}Sentiment\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/marl/ns\\PYZsh{}hasPolarity\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}marl:Positive\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.w3.org/ns/prov\\PYZsh{}wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.invalid/Analysis\\PYZus{}1554364668.1011338\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/ns\\PYZsh{}hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lJmV4cGFuZGVkPVRydWUj\",\n", + " \"@type\": [\n", + " \"http://www.gsi.upm.es/onto/senpy/ns#Results\"\n", + " ],\n", + " \"http://www.w3.org/ns/prov#used\": [\n", + " {\n", + " \"@id\": \"http://senpy.invalid/\",\n", + " \"@type\": [\n", + " \"http://www.gsi.upm.es/onto/senpy/ns#Entry\"\n", + " ],\n", + " \"http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#isString\": [\n", + " {\n", + " \"@value\": \"Senpy is awesome\"\n", + " }\n", + " ],\n", + " \"http://www.gsi.dit.upm.es/ontologies/marl/ns#hasOpinion\": [\n", + " {\n", + " \"@type\": [\n", + " \"http://www.gsi.upm.es/onto/senpy/ns#Sentiment\"\n", + " ],\n", + " \"http://www.gsi.dit.upm.es/ontologies/marl/ns#hasPolarity\": [\n", + " {\n", + " \"@value\": \"marl:Positive\"\n", + " }\n", + " ],\n", + " \"http://www.w3.org/ns/prov#wasGeneratedBy\": [\n", + " {\n", + " \"@id\": \"http://senpy.invalid/Analysis_1554364668.1011338\"\n", + " }\n", + " ]\n", + " }\n", + " ],\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/ns#hasEmotionSet\": []\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import requests\n", + "res = requests.get(f'{endpoint}/sentiment140',\n", + " params={\"input\": \"Senpy is awesome\",\n", + " \"expanded\": True})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lJmV4cGFuZGVkPVRydWUj",\n",
+       "  "@type": [\n",
+       "    "http://www.gsi.upm.es/onto/senpy/ns#Results"\n",
+       "  ],\n",
+       "  "http://www.w3.org/ns/prov#used": [\n",
+       "    {\n",
+       "      "@id": "http://senpy.invalid/",\n",
+       "      "@type": [\n",
+       "        "http://www.gsi.upm.es/onto/senpy/ns#Entry"\n",
+       "      ],\n",
+       "      "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#isString": [\n",
+       "        {\n",
+       "          "@value": "Senpy is awesome"\n",
+       "        }\n",
+       "      ],\n",
+       "      "http://www.gsi.dit.upm.es/ontologies/marl/ns#hasOpinion": [\n",
+       "        {\n",
+       "          "@type": [\n",
+       "            "http://www.gsi.upm.es/onto/senpy/ns#Sentiment"\n",
+       "          ],\n",
+       "          "http://www.gsi.dit.upm.es/ontologies/marl/ns#hasPolarity": [\n",
+       "            {\n",
+       "              "@value": "marl:Positive"\n",
+       "            }\n",
+       "          ],\n",
+       "          "http://www.w3.org/ns/prov#wasGeneratedBy": [\n",
+       "            {\n",
+       "              "@id": "http://senpy.invalid/Analysis_1554364668.1011338"\n",
+       "            }\n",
+       "          ]\n",
+       "        }\n",
+       "      ],\n",
+       "      "http://www.gsi.dit.upm.es/ontologies/onyx/ns#hasEmotionSet": []\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lJmV4cGFuZGVkPVRydWUj\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}http://www.gsi.upm.es/onto/senpy/ns\\PYZsh{}Results\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.w3.org/ns/prov\\PYZsh{}used\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.invalid/\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}http://www.gsi.upm.es/onto/senpy/ns\\PYZsh{}Entry\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://persistence.uni\\PYZhy{}leipzig.org/nlp2rdf/ontologies/nif\\PYZhy{}core\\PYZsh{}isString\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is awesome\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/marl/ns\\PYZsh{}hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}http://www.gsi.upm.es/onto/senpy/ns\\PYZsh{}Sentiment\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/marl/ns\\PYZsh{}hasPolarity\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}marl:Positive\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.w3.org/ns/prov\\PYZsh{}wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.invalid/Analysis\\PYZus{}1554364668.1011338\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/ns\\PYZsh{}hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lJmV4cGFuZGVkPVRydWUj\",\n", + " \"@type\": [\n", + " \"http://www.gsi.upm.es/onto/senpy/ns#Results\"\n", + " ],\n", + " \"http://www.w3.org/ns/prov#used\": [\n", + " {\n", + " \"@id\": \"http://senpy.invalid/\",\n", + " \"@type\": [\n", + " \"http://www.gsi.upm.es/onto/senpy/ns#Entry\"\n", + " ],\n", + " \"http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#isString\": [\n", + " {\n", + " \"@value\": \"Senpy is awesome\"\n", + " }\n", + " ],\n", + " \"http://www.gsi.dit.upm.es/ontologies/marl/ns#hasOpinion\": [\n", + " {\n", + " \"@type\": [\n", + " \"http://www.gsi.upm.es/onto/senpy/ns#Sentiment\"\n", + " ],\n", + " \"http://www.gsi.dit.upm.es/ontologies/marl/ns#hasPolarity\": [\n", + " {\n", + " \"@value\": \"marl:Positive\"\n", + " }\n", + " ],\n", + " \"http://www.w3.org/ns/prov#wasGeneratedBy\": [\n", + " {\n", + " \"@id\": \"http://senpy.invalid/Analysis_1554364668.1011338\"\n", + " }\n", + " ]\n", + " }\n", + " ],\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/ns#hasEmotionSet\": []\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Other output formats" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Senpy supports several semantic formats, like turtle and xml-RDF.\n", + "You can select the format of the output with the `outformat` parameter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
@prefix : <http://www.gsi.upm.es/onto/senpy/ns#> .\n",
+       "@prefix dc: <http://dublincore.org/2012/06/14/dcelements#> .\n",
+       "@prefix emoml: <http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/emotionml/ns#> .\n",
+       "@prefix endpoint: <http://senpy.gsi.upm.es/api/> .\n",
+       "@prefix fam: <http://vocab.fusepool.info/fam#> .\n",
+       "@prefix marl: <http://www.gsi.dit.upm.es/ontologies/marl/ns#> .\n",
+       "@prefix nif: <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#> .\n",
+       "@prefix onyx: <http://www.gsi.dit.upm.es/ontologies/onyx/ns#> .\n",
+       "@prefix prefix: <http://senpy.invalid/> .\n",
+       "@prefix prov: <http://www.w3.org/ns/prov#> .\n",
+       "@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .\n",
+       "@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .\n",
+       "@prefix senpy: <http://www.gsi.upm.es/onto/senpy/ns#> .\n",
+       "@prefix wna: <http://www.gsi.dit.upm.es/ontologies/wnaffect/ns#> .\n",
+       "@prefix xml: <http://www.w3.org/XML/1998/namespace> .\n",
+       "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .\n",
+       "\n",
+       "prefix: a senpy:Entry ;\n",
+       "    nif:isString "Senpy is the best framework for semantic sentiment analysis, and very easy to use" ;\n",
+       "    marl:hasOpinion [ a senpy:Sentiment ;\n",
+       "            marl:hasPolarity "marl:Positive" ;\n",
+       "            prov:wasGeneratedBy prefix:Analysis_1554364668.5153766 ] .\n",
+       "\n",
+       "[] a senpy:Results ;\n",
+       "    prov:used prefix: .\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.gsi.upm.es/onto/senpy/ns\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{dc:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://dublincore.org/2012/06/14/dcelements\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{emoml:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/emotionml/ns\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{endpoint:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://senpy.gsi.upm.es/api/\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{fam:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://vocab.fusepool.info/fam\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{marl:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.gsi.dit.upm.es/ontologies/marl/ns\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{nif:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://persistence.uni\\PYZhy{}leipzig.org/nlp2rdf/ontologies/nif\\PYZhy{}core\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{onyx:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.gsi.dit.upm.es/ontologies/onyx/ns\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{prefix:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://senpy.invalid/\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{prov:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.w3.org/ns/prov\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{rdf:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.w3.org/1999/02/22\\PYZhy{}rdf\\PYZhy{}syntax\\PYZhy{}ns\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{rdfs:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.w3.org/2000/01/rdf\\PYZhy{}schema\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{senpy:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.gsi.upm.es/onto/senpy/ns\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{wna:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.gsi.dit.upm.es/ontologies/wnaffect/ns\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{xml:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.w3.org/XML/1998/namespace\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\\PY{k}{@prefix}\\PY{+w}{ }\\PY{n+nn}{xsd:}\\PY{+w}{ }\\PY{n+nv}{\\PYZlt{}http://www.w3.org/2001/XMLSchema\\PYZsh{}\\PYZgt{}}\\PY{+w}{ }\\PY{p}{.}\n", + "\n", + "\\PY{err}{p}\\PY{err}{r}\\PY{err}{e}\\PY{err}{f}\\PY{err}{i}\\PY{err}{x}\\PY{p}{:}\\PY{+w}{ }\\PY{k+kt}{a}\\PY{+w}{ }\\PY{n+nn}{senpy:}\\PY{n+nt}{Entry}\\PY{+w}{ }\\PY{p}{;}\n", + "\\PY{+w}{ }\\PY{n+nn}{nif:}\\PY{n+nt}{isString}\\PY{+w}{ }\\PY{l+s}{\\PYZdq{}}\\PY{l+s}{Senpy is the best framework for semantic sentiment analysis, and very easy to use}\\PY{l+s}{\\PYZdq{}}\\PY{+w}{ }\\PY{p}{;}\n", + "\\PY{+w}{ }\\PY{n+nn}{marl:}\\PY{n+nt}{hasOpinion}\\PY{+w}{ }\\PY{p}{[}\\PY{+w}{ }\\PY{k+kt}{a}\\PY{+w}{ }\\PY{n+nn}{senpy:}\\PY{n+nt}{Sentiment}\\PY{+w}{ }\\PY{p}{;}\n", + "\\PY{+w}{ }\\PY{n+nn}{marl:}\\PY{n+nt}{hasPolarity}\\PY{+w}{ }\\PY{l+s}{\\PYZdq{}}\\PY{l+s}{marl:Positive}\\PY{l+s}{\\PYZdq{}}\\PY{+w}{ }\\PY{p}{;}\n", + "\\PY{+w}{ }\\PY{n+nn}{prov:}\\PY{n+nt}{wasGeneratedBy}\\PY{+w}{ }\\PY{n+nn}{prefix:}\\PY{n+nt}{Analysis\\PYZus{}1554364668}\\PY{l+m+mf}{.5153766}\\PY{+w}{ }\\PY{p}{]}\\PY{+w}{ }\\PY{p}{.}\n", + "\n", + "\\PY{p}{[}\\PY{p}{]}\\PY{+w}{ }\\PY{k+kt}{a}\\PY{+w}{ }\\PY{n+nn}{senpy:}\\PY{n+nt}{Results}\\PY{+w}{ }\\PY{p}{;}\n", + "\\PY{+w}{ }\\PY{n+nn}{prov:}\\PY{n+nt}{used}\\PY{+w}{ }\\PY{err}{p}\\PY{err}{r}\\PY{err}{e}\\PY{err}{f}\\PY{err}{i}\\PY{err}{x}\\PY{p}{:}\\PY{+w}{ }\\PY{p}{.}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "@prefix : .\n", + "@prefix dc: .\n", + "@prefix emoml: .\n", + "@prefix endpoint: .\n", + "@prefix fam: .\n", + "@prefix marl: .\n", + "@prefix nif: .\n", + "@prefix onyx: .\n", + "@prefix prefix: .\n", + "@prefix prov: .\n", + "@prefix rdf: .\n", + "@prefix rdfs: .\n", + "@prefix senpy: .\n", + "@prefix wna: .\n", + "@prefix xml: .\n", + "@prefix xsd: .\n", + "\n", + "prefix: a senpy:Entry ;\n", + " nif:isString \"Senpy is the best framework for semantic sentiment analysis, and very easy to use\" ;\n", + " marl:hasOpinion [ a senpy:Sentiment ;\n", + " marl:hasPolarity \"marl:Positive\" ;\n", + " prov:wasGeneratedBy prefix:Analysis_1554364668.5153766 ] .\n", + "\n", + "[] a senpy:Results ;\n", + " prov:used prefix: .\n" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/sentiment140',\n", + " params={\"input\": \"Senpy is the best framework for semantic sentiment analysis, and very easy to use\",\n", + " \"outformat\": \"turtle\"})\n", + "pretty(res.text, language='turtle')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Selecting fields from the output" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The full output in the previous sections is very useful because it is semantically annotated.\n", + "However, it is also quite verbose if we only want to label a piece of text, or get a polarity value.\n", + "\n", + "For such simple cases, the API has a special `fields` method you can use to get a specific field from the results, and even transform the results. Senpy uses jmespath under the hood, which has its own notation.\n", + "\n", + "To illustrate this, let us get only the text (`nif:isString`) from each entry:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\"Senpy is a wonderful service\"]\n" + ] + } + ], + "source": [ + "res = requests.get(f'{endpoint}/sentiment140',\n", + " params={\"input\": \"Senpy is a wonderful service\",\n", + " \"fields\": 'entries[].\"nif:isString\"'})\n", + "print(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or we could get both the text and the polarity of the text (assuming there is only one opinion per entry) with a slightly more complicated query:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\"Senpy is a service. Wonderful service.\", \"marl:Neutral\"]\n" + ] + } + ], + "source": [ + "res = requests.get(f'{endpoint}/sentiment140',\n", + " params={\"input\": \"Senpy is a service. Wonderful service.\",\n", + " \"delimiter\": \"sentence\",\n", + " \"fields\": 'entries[0].[\"nif:isString\", \"marl:hasOpinion\"[0].\"marl:hasPolarity\"]'})\n", + "print(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "jmespath is rather extensive for this tutorial. We will cover only the most simple cases, so you do not need to learn much about the notation.\n", + "\n", + "For more complicated transformations, check out [jmespath](http://jmespath.org).\n", + "In addition to a fairly complete documentation, they have a live environment you can use to test your queries." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Emotion analysis" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "Senpy uses the `onyx` vocabulary to represent emotions, which incorporates the notion of `EmotionSet`'s, an emotion that is composed of several emotions.\n", + "In a nutshell, an `Entry` is linked to one or more `EmotionSet`, which in turn is made up of one or more `Emotion`.\n", + "\n", + "Let's illustrate it with an example, using the `emotion-depechemood` plugin." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tZGVwZWNoZW1vb2Q_aW5wdXQ9U2VucHkraXMrYSt3b25kZXJmdWwrdGhhdCtzZXJ2aWNlIw%3D%3D",\n",
+       "  "@type": "Results",\n",
+       "  "entries": [\n",
+       "    {\n",
+       "      "@id": "prefix:",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [],\n",
+       "      "nif:isString": "Senpy is a wonderful that service",\n",
+       "      "onyx:hasEmotionSet": [\n",
+       "        {\n",
+       "          "@type": "EmotionSet",\n",
+       "          "onyx:hasEmotion": [\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:negative-fear",\n",
+       "              "onyx:hasEmotionIntensity": 0.06258366271018097\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:amusement",\n",
+       "              "onyx:hasEmotionIntensity": 0.15784834034155437\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:anger",\n",
+       "              "onyx:hasEmotionIntensity": 0.08728815135373413\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:annoyance",\n",
+       "              "onyx:hasEmotionIntensity": 0.12184635680460143\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:indifference",\n",
+       "              "onyx:hasEmotionIntensity": 0.1374081151031531\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:joy",\n",
+       "              "onyx:hasEmotionIntensity": 0.12267040802346799\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:awe",\n",
+       "              "onyx:hasEmotionIntensity": 0.21085262130713067\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:sadness",\n",
+       "              "onyx:hasEmotionIntensity": 0.09950234435617733\n",
+       "            }\n",
+       "          ],\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364674.7078097"\n",
+       "        }\n",
+       "      ]\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tZGVwZWNoZW1vb2Q\\PYZus{}aW5wdXQ9U2VucHkraXMrYSt3b25kZXJmdWwrdGhhdCtzZXJ2aWNlIw\\PYZpc{}3D\\PYZpc{}3D\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Results\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}entries\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is a wonderful that service\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}EmotionSet\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:negative\\PYZhy{}fear\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.06258366271018097}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:amusement\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.15784834034155437}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:anger\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.08728815135373413}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:annoyance\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.12184635680460143}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:indifference\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.1374081151031531}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:joy\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.12267040802346799}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:awe\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.21085262130713067}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:sadness\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.09950234435617733}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364674.7078097\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tZGVwZWNoZW1vb2Q_aW5wdXQ9U2VucHkraXMrYSt3b25kZXJmdWwrdGhhdCtzZXJ2aWNlIw%3D%3D\",\n", + " \"@type\": \"Results\",\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [],\n", + " \"nif:isString\": \"Senpy is a wonderful that service\",\n", + " \"onyx:hasEmotionSet\": [\n", + " {\n", + " \"@type\": \"EmotionSet\",\n", + " \"onyx:hasEmotion\": [\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:negative-fear\",\n", + " \"onyx:hasEmotionIntensity\": 0.06258366271018097\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:amusement\",\n", + " \"onyx:hasEmotionIntensity\": 0.15784834034155437\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:anger\",\n", + " \"onyx:hasEmotionIntensity\": 0.08728815135373413\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:annoyance\",\n", + " \"onyx:hasEmotionIntensity\": 0.12184635680460143\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:indifference\",\n", + " \"onyx:hasEmotionIntensity\": 0.1374081151031531\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:joy\",\n", + " \"onyx:hasEmotionIntensity\": 0.12267040802346799\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:awe\",\n", + " \"onyx:hasEmotionIntensity\": 0.21085262130713067\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:sadness\",\n", + " \"onyx:hasEmotionIntensity\": 0.09950234435617733\n", + " }\n", + " ],\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364674.7078097\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/emotion-depechemood',\n", + " params={\"input\": \"Senpy is a wonderful that service\"})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you have probably noticed, there are several emotions in this result, each with a different intensity.\n", + "\n", + "We can also tell senpy to only return the emotion with the maximum intensity using the `maxemotion` parameter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tZGVwZWNoZW1vb2Q_aW5wdXQ9U2VucHkraXMrYSt3b25kZXJmdWwrc2VydmljZSZtYXhlbW90aW9uPVRydWUj",\n",
+       "  "@type": "Results",\n",
+       "  "entries": [\n",
+       "    {\n",
+       "      "@id": "prefix:",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [],\n",
+       "      "nif:isString": "Senpy is a wonderful service",\n",
+       "      "onyx:hasEmotionSet": [\n",
+       "        {\n",
+       "          "@type": "EmotionSet",\n",
+       "          "onyx:hasEmotion": [\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:awe",\n",
+       "              "onyx:hasEmotionIntensity": 0.21085262130713067\n",
+       "            }\n",
+       "          ],\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364674.8374224"\n",
+       "        }\n",
+       "      ]\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tZGVwZWNoZW1vb2Q\\PYZus{}aW5wdXQ9U2VucHkraXMrYSt3b25kZXJmdWwrc2VydmljZSZtYXhlbW90aW9uPVRydWUj\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Results\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}entries\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is a wonderful service\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}EmotionSet\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:awe\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.21085262130713067}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364674.8374224\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tZGVwZWNoZW1vb2Q_aW5wdXQ9U2VucHkraXMrYSt3b25kZXJmdWwrc2VydmljZSZtYXhlbW90aW9uPVRydWUj\",\n", + " \"@type\": \"Results\",\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [],\n", + " \"nif:isString\": \"Senpy is a wonderful service\",\n", + " \"onyx:hasEmotionSet\": [\n", + " {\n", + " \"@type\": \"EmotionSet\",\n", + " \"onyx:hasEmotion\": [\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:awe\",\n", + " \"onyx:hasEmotionIntensity\": 0.21085262130713067\n", + " }\n", + " ],\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364674.8374224\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/emotion-depechemood',\n", + " params={\"input\": \"Senpy is a wonderful service\",\n", + " \"maxemotion\": True})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can combine this feature with the `fields` parameter to get only the label and the intensity:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
[["wna:awe", 0.21085262130713067]]\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{[}\\PY{p}{[}\\PY{l+s+s2}{\\PYZdq{}wna:awe\\PYZdq{}}\\PY{p}{,} \\PY{l+m+mf}{0.21085262130713067}\\PY{p}{]}\\PY{p}{]}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "[[\"wna:awe\", 0.21085262130713067]]" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/emotion-depechemood',\n", + " params={\"input\": \"Senpy is a wonderful service\",\n", + " \"fields\": 'entries[].\"onyx:hasEmotionSet\"[].\"onyx:hasEmotion\"[][\"onyx:hasEmotionCategory\",\"onyx:hasEmotionIntensity\"]',\n", + " \"maxemotion\": True})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Emotion conversion" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the model used by a plugin is not right for your application, you can ask for a specific emotion model in your request.\n", + "\n", + "Senpy ships with emotion conversion capabilities, and it will try to automatically convert the results.\n", + "\n", + "For example, the `emotion-anew` plugin uses the dimensional `pad` (or VAD, valence-arousal-dominance) model, as we can see here:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQj\",\n", + " \"@type\": \"Results\",\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [],\n", + " \"nif:isString\": \"Senpy is a wonderful service and I love it\",\n", + " \"onyx:hasEmotionSet\": [\n", + " {\n", + " \"@id\": \"Emotions0\",\n", + " \"@type\": \"EmotionSet\",\n", + " \"onyx:hasEmotion\": [\n", + " {\n", + " \"@id\": \"Emotion0\",\n", + " \"@type\": \"Emotion\",\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal\": 6.44,\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance\": 7.11,\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence\": 8.72,\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.1427004\"\n", + " }\n", + " ],\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.1427004\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}\n" + ] + } + ], + "source": [ + "res = requests.get(f'{endpoint}/emotion-anew',\n", + " params={\"input\": \"Senpy is a wonderful service and I love it\"})\n", + "print(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we need a category level, we can ask for the equivalent results in the `big6` model:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQmZW1vdGlvbi1tb2RlbD1lbW9tbCUzQWJpZzYj",\n",
+       "  "@type": "Results",\n",
+       "  "entries": [\n",
+       "    {\n",
+       "      "@id": "prefix:",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [],\n",
+       "      "nif:isString": "Senpy is a wonderful service and I love it",\n",
+       "      "onyx:hasEmotionSet": [\n",
+       "        {\n",
+       "          "@id": "Emotions0",\n",
+       "          "@type": "EmotionSet",\n",
+       "          "onyx:hasEmotion": [\n",
+       "            {\n",
+       "              "@id": "Emotion0",\n",
+       "              "@type": "Emotion",\n",
+       "              "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal": 6.44,\n",
+       "              "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance": 7.11,\n",
+       "              "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence": 8.72,\n",
+       "              "prov:wasGeneratedBy": "prefix:Analysis_1554364675.2834926"\n",
+       "            }\n",
+       "          ],\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364675.2834926"\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "EmotionSet",\n",
+       "          "onyx:hasEmotion": [\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:algorithmConfidence": 7.449999999999999,\n",
+       "              "onyx:hasEmotionCategory": "emoml:big6fear"\n",
+       "            }\n",
+       "          ],\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364675.2902758"\n",
+       "        }\n",
+       "      ]\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQmZW1vdGlvbi1tb2RlbD1lbW9tbCUzQWJpZzYj\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Results\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}entries\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is a wonderful service and I love it\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotions0\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}EmotionSet\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion0\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns\\PYZsh{}arousal\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{6.44}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns\\PYZsh{}dominance\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{7.11}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns\\PYZsh{}valence\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{8.72}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364675.2834926\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364675.2834926\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}EmotionSet\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:algorithmConfidence\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{7.449999999999999}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}emoml:big6fear\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364675.2902758\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQmZW1vdGlvbi1tb2RlbD1lbW9tbCUzQWJpZzYj\",\n", + " \"@type\": \"Results\",\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [],\n", + " \"nif:isString\": \"Senpy is a wonderful service and I love it\",\n", + " \"onyx:hasEmotionSet\": [\n", + " {\n", + " \"@id\": \"Emotions0\",\n", + " \"@type\": \"EmotionSet\",\n", + " \"onyx:hasEmotion\": [\n", + " {\n", + " \"@id\": \"Emotion0\",\n", + " \"@type\": \"Emotion\",\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal\": 6.44,\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance\": 7.11,\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence\": 8.72,\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.2834926\"\n", + " }\n", + " ],\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.2834926\"\n", + " },\n", + " {\n", + " \"@type\": \"EmotionSet\",\n", + " \"onyx:hasEmotion\": [\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:algorithmConfidence\": 7.449999999999999,\n", + " \"onyx:hasEmotionCategory\": \"emoml:big6fear\"\n", + " }\n", + " ],\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.2902758\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/emotion-anew',\n", + " params={\"input\": \"Senpy is a wonderful service and I love it\",\n", + " \"emotion-model\": \"emoml:big6\"})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because we don't usually care about the original emotion, the conversion can be presented in three ways:\n", + "\n", + "* full: the original and converted emotions are included at the same level\n", + "* filtered: the original emotion is replaced by the converted emotion\n", + "* nested: the original emotion is replaced, but the converted emotion points to it\n", + "\n", + "For example, here's how the `nested` structure would look like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQmZW1vdGlvbi1tb2RlbD1lbW9tbCUzQWJpZzYmY29udmVyc2lvbj1uZXN0ZWQj",\n",
+       "  "@type": "Results",\n",
+       "  "entries": [\n",
+       "    {\n",
+       "      "@id": "prefix:",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [],\n",
+       "      "nif:isString": "Senpy is a wonderful service and I love it",\n",
+       "      "onyx:hasEmotionSet": [\n",
+       "        {\n",
+       "          "@type": "EmotionSet",\n",
+       "          "onyx:hasEmotion": [\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:algorithmConfidence": 7.449999999999999,\n",
+       "              "onyx:hasEmotionCategory": "emoml:big6fear"\n",
+       "            }\n",
+       "          ],\n",
+       "          "prov:wasDerivedFrom": {\n",
+       "            "@id": "Emotions0",\n",
+       "            "@type": "EmotionSet",\n",
+       "            "onyx:hasEmotion": [\n",
+       "              {\n",
+       "                "@id": "Emotion0",\n",
+       "                "@type": "Emotion",\n",
+       "                "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal": 6.44,\n",
+       "                "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance": 7.11,\n",
+       "                "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence": 8.72,\n",
+       "                "prov:wasGeneratedBy": "prefix:Analysis_1554364675.4125388"\n",
+       "              }\n",
+       "            ],\n",
+       "            "prov:wasGeneratedBy": "prefix:Analysis_1554364675.4125388"\n",
+       "          },\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364675.4143574"\n",
+       "        }\n",
+       "      ]\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQmZW1vdGlvbi1tb2RlbD1lbW9tbCUzQWJpZzYmY29udmVyc2lvbj1uZXN0ZWQj\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Results\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}entries\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is a wonderful service and I love it\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}EmotionSet\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:algorithmConfidence\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{7.449999999999999}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}emoml:big6fear\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasDerivedFrom\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotions0\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}EmotionSet\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion0\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns\\PYZsh{}arousal\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{6.44}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns\\PYZsh{}dominance\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{7.11}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns\\PYZsh{}valence\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{8.72}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364675.4125388\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364675.4125388\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364675.4143574\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQmZW1vdGlvbi1tb2RlbD1lbW9tbCUzQWJpZzYmY29udmVyc2lvbj1uZXN0ZWQj\",\n", + " \"@type\": \"Results\",\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [],\n", + " \"nif:isString\": \"Senpy is a wonderful service and I love it\",\n", + " \"onyx:hasEmotionSet\": [\n", + " {\n", + " \"@type\": \"EmotionSet\",\n", + " \"onyx:hasEmotion\": [\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:algorithmConfidence\": 7.449999999999999,\n", + " \"onyx:hasEmotionCategory\": \"emoml:big6fear\"\n", + " }\n", + " ],\n", + " \"prov:wasDerivedFrom\": {\n", + " \"@id\": \"Emotions0\",\n", + " \"@type\": \"EmotionSet\",\n", + " \"onyx:hasEmotion\": [\n", + " {\n", + " \"@id\": \"Emotion0\",\n", + " \"@type\": \"Emotion\",\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal\": 6.44,\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance\": 7.11,\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence\": 8.72,\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.4125388\"\n", + " }\n", + " ],\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.4125388\"\n", + " },\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.4143574\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/emotion-anew',\n", + " params={\"input\": \"Senpy is a wonderful service and I love it\",\n", + " \"emotion-model\": \"emoml:big6\",\n", + " \"conversion\": \"nested\"})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Again, for completion, we could get only the label with the `fields` parameter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
[["Senpy is a wonderful service and I love it", "emoml:big6fear"]]\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{[}\\PY{p}{[}\\PY{l+s+s2}{\\PYZdq{}Senpy is a wonderful service and I love it\\PYZdq{}}\\PY{p}{,} \\PY{l+s+s2}{\\PYZdq{}emoml:big6fear\\PYZdq{}}\\PY{p}{]}\\PY{p}{]}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "[[\"Senpy is a wonderful service and I love it\", \"emoml:big6fear\"]]" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/emotion-anew',\n", + " params={\"input\": \"Senpy is a wonderful service and I love it\",\n", + " \"emotion-model\": \"emoml:big6\",\n", + " \"fields\": 'entries[].[[\"nif:isString\",\"onyx:hasEmotionSet\"[].\"onyx:hasEmotion\"[].\"onyx:hasEmotionCategory\"][]][]',\n", + " \"conversion\": \"filtered\"})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Built-in client" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The built-in senpy client allows you to query any Senpy endpoint. We will illustrate how to use it with the public demo endpoint, and then show you how to spin up your own endpoint using docker." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Building pipelines" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can query several senpy services in the same request.\n", + "This feature is called pipelining, and the result of combining several plugins in a request is called a pipeline.\n", + "\n", + "The simplest way to use pipelines is to add every plugin you want to use to the URL, separated by either a slash or a comma.\n", + "\n", + "For instance, to get sentiment (`sentiment140`) and emotion (`depechemood`) annotations at the same time:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MC9lbW90aW9uLWRlcGVjaGVtb29kP2lucHV0PVNlbnB5K2lzK2Erd29uZGVyZnVsK3NlcnZpY2Uj",\n",
+       "  "@type": "Results",\n",
+       "  "entries": [\n",
+       "    {\n",
+       "      "@id": "prefix:",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [\n",
+       "        {\n",
+       "          "@type": "Sentiment",\n",
+       "          "marl:hasPolarity": "marl:Neutral",\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364675.8928602"\n",
+       "        }\n",
+       "      ],\n",
+       "      "nif:isString": "Senpy is a wonderful service",\n",
+       "      "onyx:hasEmotionSet": [\n",
+       "        {\n",
+       "          "@type": "EmotionSet",\n",
+       "          "onyx:hasEmotion": [\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:negative-fear",\n",
+       "              "onyx:hasEmotionIntensity": 0.06258366271018097\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:amusement",\n",
+       "              "onyx:hasEmotionIntensity": 0.15784834034155437\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:anger",\n",
+       "              "onyx:hasEmotionIntensity": 0.08728815135373413\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:annoyance",\n",
+       "              "onyx:hasEmotionIntensity": 0.12184635680460143\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:indifference",\n",
+       "              "onyx:hasEmotionIntensity": 0.1374081151031531\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:joy",\n",
+       "              "onyx:hasEmotionIntensity": 0.12267040802346799\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:awe",\n",
+       "              "onyx:hasEmotionIntensity": 0.21085262130713067\n",
+       "            },\n",
+       "            {\n",
+       "              "@type": "Emotion",\n",
+       "              "onyx:hasEmotionCategory": "wna:sadness",\n",
+       "              "onyx:hasEmotionIntensity": 0.09950234435617733\n",
+       "            }\n",
+       "          ],\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364675.8937423"\n",
+       "        }\n",
+       "      ]\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MC9lbW90aW9uLWRlcGVjaGVtb29kP2lucHV0PVNlbnB5K2lzK2Erd29uZGVyZnVsK3NlcnZpY2Uj\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Results\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}entries\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Sentiment\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasPolarity\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}marl:Neutral\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364675.8928602\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is a wonderful service\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}EmotionSet\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:negative\\PYZhy{}fear\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.06258366271018097}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:amusement\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.15784834034155437}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:anger\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.08728815135373413}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:annoyance\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.12184635680460143}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:indifference\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.1374081151031531}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:joy\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.12267040802346799}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:awe\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.21085262130713067}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionCategory\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}wna:sadness\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionIntensity\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.09950234435617733}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364675.8937423\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MC9lbW90aW9uLWRlcGVjaGVtb29kP2lucHV0PVNlbnB5K2lzK2Erd29uZGVyZnVsK3NlcnZpY2Uj\",\n", + " \"@type\": \"Results\",\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [\n", + " {\n", + " \"@type\": \"Sentiment\",\n", + " \"marl:hasPolarity\": \"marl:Neutral\",\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.8928602\"\n", + " }\n", + " ],\n", + " \"nif:isString\": \"Senpy is a wonderful service\",\n", + " \"onyx:hasEmotionSet\": [\n", + " {\n", + " \"@type\": \"EmotionSet\",\n", + " \"onyx:hasEmotion\": [\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:negative-fear\",\n", + " \"onyx:hasEmotionIntensity\": 0.06258366271018097\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:amusement\",\n", + " \"onyx:hasEmotionIntensity\": 0.15784834034155437\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:anger\",\n", + " \"onyx:hasEmotionIntensity\": 0.08728815135373413\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:annoyance\",\n", + " \"onyx:hasEmotionIntensity\": 0.12184635680460143\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:indifference\",\n", + " \"onyx:hasEmotionIntensity\": 0.1374081151031531\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:joy\",\n", + " \"onyx:hasEmotionIntensity\": 0.12267040802346799\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:awe\",\n", + " \"onyx:hasEmotionIntensity\": 0.21085262130713067\n", + " },\n", + " {\n", + " \"@type\": \"Emotion\",\n", + " \"onyx:hasEmotionCategory\": \"wna:sadness\",\n", + " \"onyx:hasEmotionIntensity\": 0.09950234435617733\n", + " }\n", + " ],\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364675.8937423\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/sentiment140/emotion-depechemood',\n", + " params={\"input\": \"Senpy is a wonderful service\"})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In a senpy pipeline, the call is processed by each plugin in sequence.\n", + "The output of a plugin is used as input for the next one.\n", + "\n", + "Pipelines take the same parameters as the plugins they are made of.\n", + "For example, if we want to split the original sentence before analysing its sentiment, we can use a pipeline made out of the `split` and the `sentiment140` plugins.\n", + "\n", + "`split` takes an extra parameter (`delimiter`) to select the type of splitting (by sentence or by paragraph), and `sentiment140` takes a `language` parameter.\n", + "\n", + "This is how the request looks like:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NwbGl0L3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lLitBbmQrc2VydmljZXMrYXJlK2NvbXBvc2FibGUuJmRlbGltaXRlcj1zZW50ZW5jZSZsYW5ndWFnZT1lbiZvdXRmb3JtYXQ9anNvbi1sZCM%3D",\n",
+       "  "@type": "Results",\n",
+       "  "entries": [\n",
+       "    {\n",
+       "      "@id": "prefix:",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [\n",
+       "        {\n",
+       "          "@type": "Sentiment",\n",
+       "          "marl:hasPolarity": "marl:Positive",\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364676.2060485"\n",
+       "        }\n",
+       "      ],\n",
+       "      "nif:isString": "Senpy is awesome. And services are composable.",\n",
+       "      "onyx:hasEmotionSet": []\n",
+       "    },\n",
+       "    {\n",
+       "      "@id": "prefix:#char=0,17",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [\n",
+       "        {\n",
+       "          "@type": "Sentiment",\n",
+       "          "marl:hasPolarity": "marl:Positive",\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364676.2060485"\n",
+       "        }\n",
+       "      ],\n",
+       "      "nif:isString": "Senpy is awesome.",\n",
+       "      "onyx:hasEmotionSet": []\n",
+       "    },\n",
+       "    {\n",
+       "      "@id": "prefix:#char=18,46",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [\n",
+       "        {\n",
+       "          "@type": "Sentiment",\n",
+       "          "marl:hasPolarity": "marl:Neutral",\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364676.2060485"\n",
+       "        }\n",
+       "      ],\n",
+       "      "nif:isString": "And services are composable.",\n",
+       "      "onyx:hasEmotionSet": []\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL3NwbGl0L3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lLitBbmQrc2VydmljZXMrYXJlK2NvbXBvc2FibGUuJmRlbGltaXRlcj1zZW50ZW5jZSZsYW5ndWFnZT1lbiZvdXRmb3JtYXQ9anNvbi1sZCM\\PYZpc{}3D\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Results\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}entries\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Sentiment\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasPolarity\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}marl:Positive\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364676.2060485\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is awesome. And services are composable.\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZsh{}char=0,17\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Sentiment\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasPolarity\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}marl:Positive\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364676.2060485\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is awesome.\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZsh{}char=18,46\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Sentiment\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasPolarity\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}marl:Neutral\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364676.2060485\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}And services are composable.\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL3NwbGl0L3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lLitBbmQrc2VydmljZXMrYXJlK2NvbXBvc2FibGUuJmRlbGltaXRlcj1zZW50ZW5jZSZsYW5ndWFnZT1lbiZvdXRmb3JtYXQ9anNvbi1sZCM%3D\",\n", + " \"@type\": \"Results\",\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [\n", + " {\n", + " \"@type\": \"Sentiment\",\n", + " \"marl:hasPolarity\": \"marl:Positive\",\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364676.2060485\"\n", + " }\n", + " ],\n", + " \"nif:isString\": \"Senpy is awesome. And services are composable.\",\n", + " \"onyx:hasEmotionSet\": []\n", + " },\n", + " {\n", + " \"@id\": \"prefix:#char=0,17\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [\n", + " {\n", + " \"@type\": \"Sentiment\",\n", + " \"marl:hasPolarity\": \"marl:Positive\",\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364676.2060485\"\n", + " }\n", + " ],\n", + " \"nif:isString\": \"Senpy is awesome.\",\n", + " \"onyx:hasEmotionSet\": []\n", + " },\n", + " {\n", + " \"@id\": \"prefix:#char=18,46\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [\n", + " {\n", + " \"@type\": \"Sentiment\",\n", + " \"marl:hasPolarity\": \"marl:Neutral\",\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364676.2060485\"\n", + " }\n", + " ],\n", + " \"nif:isString\": \"And services are composable.\",\n", + " \"onyx:hasEmotionSet\": []\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/split/sentiment140',\n", + " params={\"input\": \"Senpy is awesome. And services are composable.\", \n", + " \"delimiter\": \"sentence\",\n", + " \"language\": \"en\",\n", + " \"outformat\": \"json-ld\"})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, `split` creates two new entries, which are also annotated by `sentiment140`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once again, we could use the `fields` parameter to get a list of strings and labels:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
[["Senpy is awesome. And services are composable.", "marl:Positive"], ["Senpy is awesome.", "marl:Positive"], ["And services are composable.", "marl:Neutral"]]\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{[}\\PY{p}{[}\\PY{l+s+s2}{\\PYZdq{}Senpy is awesome. And services are composable.\\PYZdq{}}\\PY{p}{,} \\PY{l+s+s2}{\\PYZdq{}marl:Positive\\PYZdq{}}\\PY{p}{]}\\PY{p}{,} \\PY{p}{[}\\PY{l+s+s2}{\\PYZdq{}Senpy is awesome.\\PYZdq{}}\\PY{p}{,} \\PY{l+s+s2}{\\PYZdq{}marl:Positive\\PYZdq{}}\\PY{p}{]}\\PY{p}{,} \\PY{p}{[}\\PY{l+s+s2}{\\PYZdq{}And services are composable.\\PYZdq{}}\\PY{p}{,} \\PY{l+s+s2}{\\PYZdq{}marl:Neutral\\PYZdq{}}\\PY{p}{]}\\PY{p}{]}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "[[\"Senpy is awesome. And services are composable.\", \"marl:Positive\"], [\"Senpy is awesome.\", \"marl:Positive\"], [\"And services are composable.\", \"marl:Neutral\"]]" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/split/sentiment140',\n", + " params={\"input\": \"Senpy is awesome. And services are composable.\", \n", + " \"delimiter\": \"sentence\",\n", + " \"fields\": 'entries[].[[\"nif:isString\",\"marl:hasOpinion\"[].\"marl:hasPolarity\"][]][]',\n", + " \"language\": \"en\",\n", + " \"outformat\": \"json-ld\"})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Evaluation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sentiment analysis plugins can also be evaluated on a series of pre-defined datasets, using the `gsitk` tool.\n", + "\n", + "For instance, to evaluate the `sentiment-vader` plugin on the `vader` and `sts` datasets, we would simply call:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2V2YWx1YXRlLz9hbGdvPXNlbnRpbWVudC12YWRlciZkYXRhc2V0PXZhZGVyJTJDc3RzJm91dGZvcm1hdD1qc29uLWxkIw%3D%3D",\n",
+       "  "@type": "AggregatedEvaluation",\n",
+       "  "senpy:evaluations": [\n",
+       "    {\n",
+       "      "@type": "Evaluation",\n",
+       "      "evaluates": "endpoint:plugins/sentiment-vader_0.1.1__vader",\n",
+       "      "evaluatesOn": "vader",\n",
+       "      "metrics": [\n",
+       "        {\n",
+       "          "@type": "Accuracy",\n",
+       "          "value": 0.6907142857142857\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Precision_macro",\n",
+       "          "value": 0.34535714285714286\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Recall_macro",\n",
+       "          "value": 0.5\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "F1_macro",\n",
+       "          "value": 0.40853400929446554\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "F1_weighted",\n",
+       "          "value": 0.5643605528396403\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "F1_micro",\n",
+       "          "value": 0.6907142857142857\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "F1_macro",\n",
+       "          "value": 0.40853400929446554\n",
+       "        }\n",
+       "      ]\n",
+       "    },\n",
+       "    {\n",
+       "      "@type": "Evaluation",\n",
+       "      "evaluates": "endpoint:plugins/sentiment-vader_0.1.1__sts",\n",
+       "      "evaluatesOn": "sts",\n",
+       "      "metrics": [\n",
+       "        {\n",
+       "          "@type": "Accuracy",\n",
+       "          "value": 0.3107177974434612\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Precision_macro",\n",
+       "          "value": 0.1553588987217306\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Recall_macro",\n",
+       "          "value": 0.5\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "F1_macro",\n",
+       "          "value": 0.23705926481620407\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "F1_weighted",\n",
+       "          "value": 0.14731706525451424\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "F1_micro",\n",
+       "          "value": 0.3107177974434612\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "F1_macro",\n",
+       "          "value": 0.23705926481620407\n",
+       "        }\n",
+       "      ]\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL2V2YWx1YXRlLz9hbGdvPXNlbnRpbWVudC12YWRlciZkYXRhc2V0PXZhZGVyJTJDc3RzJm91dGZvcm1hdD1qc29uLWxkIw\\PYZpc{}3D\\PYZpc{}3D\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}AggregatedEvaluation\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}senpy:evaluations\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Evaluation\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}evaluates\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}endpoint:plugins/sentiment\\PYZhy{}vader\\PYZus{}0.1.1\\PYZus{}\\PYZus{}vader\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}evaluatesOn\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}vader\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}metrics\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Accuracy\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.6907142857142857}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Precision\\PYZus{}macro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.34535714285714286}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Recall\\PYZus{}macro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.5}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}F1\\PYZus{}macro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.40853400929446554}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}F1\\PYZus{}weighted\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.5643605528396403}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}F1\\PYZus{}micro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.6907142857142857}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}F1\\PYZus{}macro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.40853400929446554}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Evaluation\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}evaluates\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}endpoint:plugins/sentiment\\PYZhy{}vader\\PYZus{}0.1.1\\PYZus{}\\PYZus{}sts\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}evaluatesOn\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}sts\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}metrics\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Accuracy\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.3107177974434612}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Precision\\PYZus{}macro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.1553588987217306}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Recall\\PYZus{}macro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.5}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}F1\\PYZus{}macro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.23705926481620407}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}F1\\PYZus{}weighted\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.14731706525451424}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}F1\\PYZus{}micro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.3107177974434612}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}F1\\PYZus{}macro\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{0.23705926481620407}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL2V2YWx1YXRlLz9hbGdvPXNlbnRpbWVudC12YWRlciZkYXRhc2V0PXZhZGVyJTJDc3RzJm91dGZvcm1hdD1qc29uLWxkIw%3D%3D\",\n", + " \"@type\": \"AggregatedEvaluation\",\n", + " \"senpy:evaluations\": [\n", + " {\n", + " \"@type\": \"Evaluation\",\n", + " \"evaluates\": \"endpoint:plugins/sentiment-vader_0.1.1__vader\",\n", + " \"evaluatesOn\": \"vader\",\n", + " \"metrics\": [\n", + " {\n", + " \"@type\": \"Accuracy\",\n", + " \"value\": 0.6907142857142857\n", + " },\n", + " {\n", + " \"@type\": \"Precision_macro\",\n", + " \"value\": 0.34535714285714286\n", + " },\n", + " {\n", + " \"@type\": \"Recall_macro\",\n", + " \"value\": 0.5\n", + " },\n", + " {\n", + " \"@type\": \"F1_macro\",\n", + " \"value\": 0.40853400929446554\n", + " },\n", + " {\n", + " \"@type\": \"F1_weighted\",\n", + " \"value\": 0.5643605528396403\n", + " },\n", + " {\n", + " \"@type\": \"F1_micro\",\n", + " \"value\": 0.6907142857142857\n", + " },\n", + " {\n", + " \"@type\": \"F1_macro\",\n", + " \"value\": 0.40853400929446554\n", + " }\n", + " ]\n", + " },\n", + " {\n", + " \"@type\": \"Evaluation\",\n", + " \"evaluates\": \"endpoint:plugins/sentiment-vader_0.1.1__sts\",\n", + " \"evaluatesOn\": \"sts\",\n", + " \"metrics\": [\n", + " {\n", + " \"@type\": \"Accuracy\",\n", + " \"value\": 0.3107177974434612\n", + " },\n", + " {\n", + " \"@type\": \"Precision_macro\",\n", + " \"value\": 0.1553588987217306\n", + " },\n", + " {\n", + " \"@type\": \"Recall_macro\",\n", + " \"value\": 0.5\n", + " },\n", + " {\n", + " \"@type\": \"F1_macro\",\n", + " \"value\": 0.23705926481620407\n", + " },\n", + " {\n", + " \"@type\": \"F1_weighted\",\n", + " \"value\": 0.14731706525451424\n", + " },\n", + " {\n", + " \"@type\": \"F1_micro\",\n", + " \"value\": 0.3107177974434612\n", + " },\n", + " {\n", + " \"@type\": \"F1_macro\",\n", + " \"value\": 0.23705926481620407\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = requests.get(f'{endpoint}/evaluate',\n", + " params={\"algo\": \"sentiment-vader\",\n", + " \"dataset\": \"vader,sts\",\n", + " 'outformat': 'json-ld'\n", + " })\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The same results can be visualized as a table in the Web interface:\n", + "\n", + "![](evaluation-results.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**note**: to evaluate a plugin on a dataset, senpy will need to predict the labels of the entries using the plugin.\n", + "This process might take long for plugins that use an external service, such as `sentiment140`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advanced topics" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Verbose output" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default, senpy does not include information that might be too verbose, such as the parameters that were used in the analysis.\n", + "\n", + "You can instruct senpy to provide a more verbose output with the `verbose` parameter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcyt0aGUrYmVzdCtmcmFtZXdvcmsrZm9yK3NlbWFudGljK3NlbnRpbWVudCthbmFseXNpcyUyQythbmQrdmVyeStlYXN5K3RvK3VzZSZ2ZXJib3NlPVRydWUj",\n",
+       "  "@type": "Results",\n",
+       "  "activities": [\n",
+       "    {\n",
+       "      "@id": "prefix:Analysis_1554364688.7944896",\n",
+       "      "@type": "Analysis",\n",
+       "      "marl:maxPolarityValue": 1,\n",
+       "      "marl:minPolarityValue": 0,\n",
+       "      "prov:used": [\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "input",\n",
+       "          "value": "Senpy is the best framework for semantic sentiment analysis, and very easy to use"\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "verbose",\n",
+       "          "value": true\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "in-headers",\n",
+       "          "value": false\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "algorithm",\n",
+       "          "value": "default"\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "expanded-jsonld",\n",
+       "          "value": false\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "with-parameters",\n",
+       "          "value": false\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "outformat",\n",
+       "          "value": "json-ld"\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "help",\n",
+       "          "value": false\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "aliases",\n",
+       "          "value": false\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "conversion",\n",
+       "          "value": "full"\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "intype",\n",
+       "          "value": "direct"\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "informat",\n",
+       "          "value": "text"\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "prefix",\n",
+       "          "value": ""\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "urischeme",\n",
+       "          "value": "RFC5147String"\n",
+       "        },\n",
+       "        {\n",
+       "          "@type": "Parameter",\n",
+       "          "name": "language",\n",
+       "          "value": "auto"\n",
+       "        }\n",
+       "      ],\n",
+       "      "prov:wasAssociatedWith": "endpoint:plugins/sentiment140_0.2"\n",
+       "    }\n",
+       "  ],\n",
+       "  "entries": [\n",
+       "    {\n",
+       "      "@id": "prefix:",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [\n",
+       "        {\n",
+       "          "@type": "Sentiment",\n",
+       "          "marl:hasPolarity": "marl:Positive",\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364688.7944896"\n",
+       "        }\n",
+       "      ],\n",
+       "      "nif:isString": "Senpy is the best framework for semantic sentiment analysis, and very easy to use",\n",
+       "      "onyx:hasEmotionSet": []\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcyt0aGUrYmVzdCtmcmFtZXdvcmsrZm9yK3NlbWFudGljK3NlbnRpbWVudCthbmFseXNpcyUyQythbmQrdmVyeStlYXN5K3RvK3VzZSZ2ZXJib3NlPVRydWUj\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Results\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}activities\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364688.7944896\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Analysis\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:maxPolarityValue\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mi}{1}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:minPolarityValue\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mi}{0}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:used\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}input\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is the best framework for semantic sentiment analysis, and very easy to use\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}verbose\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}in\\PYZhy{}headers\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}algorithm\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}default\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}expanded\\PYZhy{}jsonld\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}with\\PYZhy{}parameters\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}outformat\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}json\\PYZhy{}ld\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}help\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}conversion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}full\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}intype\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}direct\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}informat\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}text\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}urischeme\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}RFC5147String\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Parameter\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}name\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}language\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}value\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}auto\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasAssociatedWith\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}endpoint:plugins/sentiment140\\PYZus{}0.2\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}entries\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Sentiment\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasPolarity\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}marl:Positive\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364688.7944896\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Senpy is the best framework for semantic sentiment analysis, and very easy to use\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcyt0aGUrYmVzdCtmcmFtZXdvcmsrZm9yK3NlbWFudGljK3NlbnRpbWVudCthbmFseXNpcyUyQythbmQrdmVyeStlYXN5K3RvK3VzZSZ2ZXJib3NlPVRydWUj\",\n", + " \"@type\": \"Results\",\n", + " \"activities\": [\n", + " {\n", + " \"@id\": \"prefix:Analysis_1554364688.7944896\",\n", + " \"@type\": \"Analysis\",\n", + " \"marl:maxPolarityValue\": 1,\n", + " \"marl:minPolarityValue\": 0,\n", + " \"prov:used\": [\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"input\",\n", + " \"value\": \"Senpy is the best framework for semantic sentiment analysis, and very easy to use\"\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"verbose\",\n", + " \"value\": true\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"in-headers\",\n", + " \"value\": false\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"algorithm\",\n", + " \"value\": \"default\"\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"expanded-jsonld\",\n", + " \"value\": false\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"with-parameters\",\n", + " \"value\": false\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"outformat\",\n", + " \"value\": \"json-ld\"\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"help\",\n", + " \"value\": false\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"aliases\",\n", + " \"value\": false\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"conversion\",\n", + " \"value\": \"full\"\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"intype\",\n", + " \"value\": \"direct\"\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"informat\",\n", + " \"value\": \"text\"\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"prefix\",\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"urischeme\",\n", + " \"value\": \"RFC5147String\"\n", + " },\n", + " {\n", + " \"@type\": \"Parameter\",\n", + " \"name\": \"language\",\n", + " \"value\": \"auto\"\n", + " }\n", + " ],\n", + " \"prov:wasAssociatedWith\": \"endpoint:plugins/sentiment140_0.2\"\n", + " }\n", + " ],\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [\n", + " {\n", + " \"@type\": \"Sentiment\",\n", + " \"marl:hasPolarity\": \"marl:Positive\",\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364688.7944896\"\n", + " }\n", + " ],\n", + " \"nif:isString\": \"Senpy is the best framework for semantic sentiment analysis, and very easy to use\",\n", + " \"onyx:hasEmotionSet\": []\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import requests\n", + "res = requests.get(f'{endpoint}/sentiment140',\n", + " params={\n", + " \"input\": \"Senpy is the best framework for semantic sentiment analysis, and very easy to use\",\n", + " \"verbose\": True}).text\n", + "pretty(res)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Getting help" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpLz9oZWxwPVRydWUj",\n",
+       "  "@type": "Help",\n",
+       "  "valid_parameters": {\n",
+       "    "algorithm": {\n",
+       "      "aliases": [\n",
+       "        "algorithms",\n",
+       "        "a",\n",
+       "        "algo"\n",
+       "      ],\n",
+       "      "default": "default",\n",
+       "      "description": "Algorithms that will be used to process the request.It may be a list of comma-separated names.",\n",
+       "      "processor": "string_to_tuple",\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "aliases": {\n",
+       "      "@id": "aliases",\n",
+       "      "aliases": [],\n",
+       "      "default": false,\n",
+       "      "description": "Replace JSON properties with their aliases",\n",
+       "      "options": [\n",
+       "        true,\n",
+       "        false\n",
+       "      ],\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "conversion": {\n",
+       "      "@id": "conversion",\n",
+       "      "default": "full",\n",
+       "      "description": "How to show the elements that have (not) been converted.\\n\\n* full: converted and original elements will appear side-by-side\\n* filtered: only converted elements will be shown\\n* nested: converted elements will be shown, and they will include a link to the original element\\n(using `prov:wasGeneratedBy`).\\n",\n",
+       "      "options": [\n",
+       "        "filtered",\n",
+       "        "nested",\n",
+       "        "full"\n",
+       "      ],\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "emotion-model": {\n",
+       "      "@id": "emotionModel",\n",
+       "      "aliases": [\n",
+       "        "emoModel",\n",
+       "        "emotionModel"\n",
+       "      ],\n",
+       "      "description": "Emotion model to use in the response.\\nSenpy will try to convert the output to this model automatically.\\n\\nExamples: `wna:liking` and `emoml:big6`.\\n        ",\n",
+       "      "required": false\n",
+       "    },\n",
+       "    "expanded-jsonld": {\n",
+       "      "@id": "expanded-jsonld",\n",
+       "      "aliases": [\n",
+       "        "expanded",\n",
+       "        "expanded_jsonld"\n",
+       "      ],\n",
+       "      "default": false,\n",
+       "      "description": "use JSON-LD expansion to get full URIs",\n",
+       "      "options": [\n",
+       "        true,\n",
+       "        false\n",
+       "      ],\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "fields": {\n",
+       "      "@id": "fields",\n",
+       "      "description": "A jmespath selector, that can be used to extract a new dictionary, array or value\\nfrom the results.\\njmespath is a powerful query language for json and/or dictionaries.\\nIt allows you to change the structure (and data) of your objects through queries.\\n\\ne.g., the following expression gets a list of `[emotion label, intensity]` for each entry:\\n`entries[].\\"onyx:hasEmotionSet\\"[].\\"onyx:hasEmotion\\"[][\\"onyx:hasEmotionCategory\\",\\"onyx:hasEmotionIntensity\\"]`\\n\\nFor more information, see: https://jmespath.org\\n\\n",\n",
+       "      "required": false\n",
+       "    },\n",
+       "    "help": {\n",
+       "      "@id": "help",\n",
+       "      "aliases": [\n",
+       "        "h"\n",
+       "      ],\n",
+       "      "default": false,\n",
+       "      "description": "Show additional help to know more about the possible parameters",\n",
+       "      "options": [\n",
+       "        true,\n",
+       "        false\n",
+       "      ],\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "in-headers": {\n",
+       "      "aliases": [\n",
+       "        "headers",\n",
+       "        "inheaders",\n",
+       "        "inHeaders",\n",
+       "        "in-headers",\n",
+       "        "in_headers"\n",
+       "      ],\n",
+       "      "default": false,\n",
+       "      "description": "Only include the JSON-LD context in the headers",\n",
+       "      "options": [\n",
+       "        true,\n",
+       "        false\n",
+       "      ],\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "informat": {\n",
+       "      "@id": "informat",\n",
+       "      "aliases": [\n",
+       "        "f"\n",
+       "      ],\n",
+       "      "default": "text",\n",
+       "      "description": "input format",\n",
+       "      "options": [\n",
+       "        "text",\n",
+       "        "json-ld"\n",
+       "      ],\n",
+       "      "required": false\n",
+       "    },\n",
+       "    "input": {\n",
+       "      "@id": "input",\n",
+       "      "aliases": [\n",
+       "        "i"\n",
+       "      ],\n",
+       "      "help": "Input text",\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "intype": {\n",
+       "      "@id": "intype",\n",
+       "      "aliases": [\n",
+       "        "t"\n",
+       "      ],\n",
+       "      "default": "direct",\n",
+       "      "description": "input type",\n",
+       "      "options": [\n",
+       "        "direct",\n",
+       "        "url",\n",
+       "        "file"\n",
+       "      ],\n",
+       "      "required": false\n",
+       "    },\n",
+       "    "language": {\n",
+       "      "aliases": [\n",
+       "        "language",\n",
+       "        "l"\n",
+       "      ],\n",
+       "      "default": "en",\n",
+       "      "description": "language of the input",\n",
+       "      "options": [\n",
+       "        "es",\n",
+       "        "en"\n",
+       "      ],\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "outformat": {\n",
+       "      "@id": "outformat",\n",
+       "      "aliases": [\n",
+       "        "o"\n",
+       "      ],\n",
+       "      "default": "json-ld",\n",
+       "      "description": "The data can be semantically formatted (JSON-LD, turtle or n-triples),\\ngiven as a list of comma-separated fields (see the fields option) or constructed from a Jinja2\\ntemplate (see the template option).",\n",
+       "      "options": [\n",
+       "        "json-ld",\n",
+       "        "turtle",\n",
+       "        "ntriples"\n",
+       "      ],\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "prefix": {\n",
+       "      "@id": "prefix",\n",
+       "      "aliases": [\n",
+       "        "p"\n",
+       "      ],\n",
+       "      "default": "",\n",
+       "      "description": "prefix to use for new entities",\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "template": {\n",
+       "      "@id": "template",\n",
+       "      "description": "Jinja2 template for the result. The input data for the template will\\nbe the results as a dictionary.\\nFor example:\\n\\nConsider the results before templating:\\n\\n```\\n[{\\n    \\"@type\\": \\"entry\\",\\n    \\"onyx:hasEmotionSet\\": [],\\n    \\"nif:isString\\": \\"testing the template\\",\\n    \\"marl:hasOpinion\\": [\\n        {\\n            \\"@type\\": \\"sentiment\\",\\n            \\"marl:hasPolarity\\": \\"marl:Positive\\"\\n        }\\n    ]\\n}]\\n```\\n\\n\\nAnd the template:\\n\\n```\\n{% for entry in entries %}\\n{{ entry[\\"nif:isString\\"] | upper }},{{entry.sentiments[0][\\"marl:hasPolarity\\"].split(\\":\\")[1]}}\\n{% endfor %}\\n```\\n\\nThe final result would be:\\n\\n```\\nTESTING THE TEMPLATE,Positive\\n```\\n",\n",
+       "      "required": false\n",
+       "    },\n",
+       "    "urischeme": {\n",
+       "      "@id": "urischeme",\n",
+       "      "aliases": [\n",
+       "        "u"\n",
+       "      ],\n",
+       "      "default": "RFC5147String",\n",
+       "      "description": "scheme for NIF URIs",\n",
+       "      "options": [\n",
+       "        "RFC5147String"\n",
+       "      ],\n",
+       "      "required": false\n",
+       "    },\n",
+       "    "verbose": {\n",
+       "      "@id": "verbose",\n",
+       "      "aliases": [\n",
+       "        "v"\n",
+       "      ],\n",
+       "      "default": false,\n",
+       "      "description": "Show all properties in the result",\n",
+       "      "options": [\n",
+       "        true,\n",
+       "        false\n",
+       "      ],\n",
+       "      "required": true\n",
+       "    },\n",
+       "    "with-parameters": {\n",
+       "      "aliases": [\n",
+       "        "withparameters",\n",
+       "        "with_parameters"\n",
+       "      ],\n",
+       "      "default": false,\n",
+       "      "description": "include initial parameters in the response",\n",
+       "      "options": [\n",
+       "        true,\n",
+       "        false\n",
+       "      ],\n",
+       "      "required": true\n",
+       "    }\n",
+       "  }\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@context\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}http://senpy.gsi.upm.es/api/contexts/YXBpLz9oZWxwPVRydWUj\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Help\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}valid\\PYZus{}parameters\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nt}{\\PYZdq{}algorithm\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}algorithms\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}a\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}algo\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Algorithms that will be used to process the request.It may be a list of comma\\PYZhy{}separated names.\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}processor\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}string\\PYZus{}to\\PYZus{}tuple\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Replace JSON properties with their aliases\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{k+kc}{true}\\PY{p}{,}\n", + " \\PY{k+kc}{false}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}conversion\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}conversion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}full\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}How to show the elements that have (not) been converted.\\PYZbs{}n\\PYZbs{}n* full: converted and original elements will appear side\\PYZhy{}by\\PYZhy{}side\\PYZbs{}n* filtered: only converted elements will be shown\\PYZbs{}n* nested: converted elements will be shown, and they will include a link to the original element\\PYZbs{}n(using `prov:wasGeneratedBy`).\\PYZbs{}n\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}filtered\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}nested\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}full\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}emotion\\PYZhy{}model\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}emotionModel\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}emoModel\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}emotionModel\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion model to use in the response.\\PYZbs{}nSenpy will try to convert the output to this model automatically.\\PYZbs{}n\\PYZbs{}nExamples: `wna:liking` and `emoml:big6`.\\PYZbs{}n \\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}expanded\\PYZhy{}jsonld\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}expanded\\PYZhy{}jsonld\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}expanded\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}expanded\\PYZus{}jsonld\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}use JSON\\PYZhy{}LD expansion to get full URIs\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{k+kc}{true}\\PY{p}{,}\n", + " \\PY{k+kc}{false}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}fields\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}fields\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}A jmespath selector, that can be used to extract a new dictionary, array or value\\PYZbs{}nfrom the results.\\PYZbs{}njmespath is a powerful query language for json and/or dictionaries.\\PYZbs{}nIt allows you to change the structure (and data) of your objects through queries.\\PYZbs{}n\\PYZbs{}ne.g., the following expression gets a list of `[emotion label, intensity]` for each entry:\\PYZbs{}n`entries[].\\PYZbs{}\\PYZdq{}onyx:hasEmotionSet\\PYZbs{}\\PYZdq{}[].\\PYZbs{}\\PYZdq{}onyx:hasEmotion\\PYZbs{}\\PYZdq{}[][\\PYZbs{}\\PYZdq{}onyx:hasEmotionCategory\\PYZbs{}\\PYZdq{},\\PYZbs{}\\PYZdq{}onyx:hasEmotionIntensity\\PYZbs{}\\PYZdq{}]`\\PYZbs{}n\\PYZbs{}nFor more information, see: https://jmespath.org\\PYZbs{}n\\PYZbs{}n\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}help\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}help\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}h\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Show additional help to know more about the possible parameters\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{k+kc}{true}\\PY{p}{,}\n", + " \\PY{k+kc}{false}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}in\\PYZhy{}headers\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}headers\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}inheaders\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}inHeaders\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}in\\PYZhy{}headers\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}in\\PYZus{}headers\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Only include the JSON\\PYZhy{}LD context in the headers\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{k+kc}{true}\\PY{p}{,}\n", + " \\PY{k+kc}{false}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}informat\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}informat\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}f\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}text\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}input format\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}text\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}json\\PYZhy{}ld\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}input\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}input\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}i\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}help\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Input text\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}intype\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}intype\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}t\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}direct\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}input type\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}direct\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}url\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}file\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}language\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}language\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}l\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}en\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}language of the input\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}es\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}en\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}outformat\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}outformat\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}o\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}json\\PYZhy{}ld\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}The data can be semantically formatted (JSON\\PYZhy{}LD, turtle or n\\PYZhy{}triples),\\PYZbs{}ngiven as a list of comma\\PYZhy{}separated fields (see the fields option) or constructed from a Jinja2\\PYZbs{}ntemplate (see the template option).\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}json\\PYZhy{}ld\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}turtle\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}ntriples\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prefix\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}p\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix to use for new entities\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}template\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}template\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Jinja2 template for the result. The input data for the template will\\PYZbs{}nbe the results as a dictionary.\\PYZbs{}nFor example:\\PYZbs{}n\\PYZbs{}nConsider the results before templating:\\PYZbs{}n\\PYZbs{}n```\\PYZbs{}n[\\PYZob{}\\PYZbs{}n \\PYZbs{}\\PYZdq{}@type\\PYZbs{}\\PYZdq{}: \\PYZbs{}\\PYZdq{}entry\\PYZbs{}\\PYZdq{},\\PYZbs{}n \\PYZbs{}\\PYZdq{}onyx:hasEmotionSet\\PYZbs{}\\PYZdq{}: [],\\PYZbs{}n \\PYZbs{}\\PYZdq{}nif:isString\\PYZbs{}\\PYZdq{}: \\PYZbs{}\\PYZdq{}testing the template\\PYZbs{}\\PYZdq{},\\PYZbs{}n \\PYZbs{}\\PYZdq{}marl:hasOpinion\\PYZbs{}\\PYZdq{}: [\\PYZbs{}n \\PYZob{}\\PYZbs{}n \\PYZbs{}\\PYZdq{}@type\\PYZbs{}\\PYZdq{}: \\PYZbs{}\\PYZdq{}sentiment\\PYZbs{}\\PYZdq{},\\PYZbs{}n \\PYZbs{}\\PYZdq{}marl:hasPolarity\\PYZbs{}\\PYZdq{}: \\PYZbs{}\\PYZdq{}marl:Positive\\PYZbs{}\\PYZdq{}\\PYZbs{}n \\PYZcb{}\\PYZbs{}n ]\\PYZbs{}n\\PYZcb{}]\\PYZbs{}n```\\PYZbs{}n\\PYZbs{}n\\PYZbs{}nAnd the template:\\PYZbs{}n\\PYZbs{}n```\\PYZbs{}n\\PYZob{}\\PYZpc{} for entry in entries \\PYZpc{}\\PYZcb{}\\PYZbs{}n\\PYZob{}\\PYZob{} entry[\\PYZbs{}\\PYZdq{}nif:isString\\PYZbs{}\\PYZdq{}] | upper \\PYZcb{}\\PYZcb{},\\PYZob{}\\PYZob{}entry.sentiments[0][\\PYZbs{}\\PYZdq{}marl:hasPolarity\\PYZbs{}\\PYZdq{}].split(\\PYZbs{}\\PYZdq{}:\\PYZbs{}\\PYZdq{})[1]\\PYZcb{}\\PYZcb{}\\PYZbs{}n\\PYZob{}\\PYZpc{} endfor \\PYZpc{}\\PYZcb{}\\PYZbs{}n```\\PYZbs{}n\\PYZbs{}nThe final result would be:\\PYZbs{}n\\PYZbs{}n```\\PYZbs{}nTESTING THE TEMPLATE,Positive\\PYZbs{}n```\\PYZbs{}n\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}urischeme\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}urischeme\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}u\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}RFC5147String\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}scheme for NIF URIs\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}RFC5147String\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}verbose\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}verbose\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}v\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Show all properties in the result\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{k+kc}{true}\\PY{p}{,}\n", + " \\PY{k+kc}{false}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}with\\PYZhy{}parameters\\PYZdq{}}\\PY{p}{:} \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nt}{\\PYZdq{}aliases\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{l+s+s2}{\\PYZdq{}withparameters\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{l+s+s2}{\\PYZdq{}with\\PYZus{}parameters\\PYZdq{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}default\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{false}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}description\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}include initial parameters in the response\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}options\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{k+kc}{true}\\PY{p}{,}\n", + " \\PY{k+kc}{false}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}required\\PYZdq{}}\\PY{p}{:} \\PY{k+kc}{true}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@context\": \"http://senpy.gsi.upm.es/api/contexts/YXBpLz9oZWxwPVRydWUj\",\n", + " \"@type\": \"Help\",\n", + " \"valid_parameters\": {\n", + " \"algorithm\": {\n", + " \"aliases\": [\n", + " \"algorithms\",\n", + " \"a\",\n", + " \"algo\"\n", + " ],\n", + " \"default\": \"default\",\n", + " \"description\": \"Algorithms that will be used to process the request.It may be a list of comma-separated names.\",\n", + " \"processor\": \"string_to_tuple\",\n", + " \"required\": true\n", + " },\n", + " \"aliases\": {\n", + " \"@id\": \"aliases\",\n", + " \"aliases\": [],\n", + " \"default\": false,\n", + " \"description\": \"Replace JSON properties with their aliases\",\n", + " \"options\": [\n", + " true,\n", + " false\n", + " ],\n", + " \"required\": true\n", + " },\n", + " \"conversion\": {\n", + " \"@id\": \"conversion\",\n", + " \"default\": \"full\",\n", + " \"description\": \"How to show the elements that have (not) been converted.\\n\\n* full: converted and original elements will appear side-by-side\\n* filtered: only converted elements will be shown\\n* nested: converted elements will be shown, and they will include a link to the original element\\n(using `prov:wasGeneratedBy`).\\n\",\n", + " \"options\": [\n", + " \"filtered\",\n", + " \"nested\",\n", + " \"full\"\n", + " ],\n", + " \"required\": true\n", + " },\n", + " \"emotion-model\": {\n", + " \"@id\": \"emotionModel\",\n", + " \"aliases\": [\n", + " \"emoModel\",\n", + " \"emotionModel\"\n", + " ],\n", + " \"description\": \"Emotion model to use in the response.\\nSenpy will try to convert the output to this model automatically.\\n\\nExamples: `wna:liking` and `emoml:big6`.\\n \",\n", + " \"required\": false\n", + " },\n", + " \"expanded-jsonld\": {\n", + " \"@id\": \"expanded-jsonld\",\n", + " \"aliases\": [\n", + " \"expanded\",\n", + " \"expanded_jsonld\"\n", + " ],\n", + " \"default\": false,\n", + " \"description\": \"use JSON-LD expansion to get full URIs\",\n", + " \"options\": [\n", + " true,\n", + " false\n", + " ],\n", + " \"required\": true\n", + " },\n", + " \"fields\": {\n", + " \"@id\": \"fields\",\n", + " \"description\": \"A jmespath selector, that can be used to extract a new dictionary, array or value\\nfrom the results.\\njmespath is a powerful query language for json and/or dictionaries.\\nIt allows you to change the structure (and data) of your objects through queries.\\n\\ne.g., the following expression gets a list of `[emotion label, intensity]` for each entry:\\n`entries[].\\\"onyx:hasEmotionSet\\\"[].\\\"onyx:hasEmotion\\\"[][\\\"onyx:hasEmotionCategory\\\",\\\"onyx:hasEmotionIntensity\\\"]`\\n\\nFor more information, see: https://jmespath.org\\n\\n\",\n", + " \"required\": false\n", + " },\n", + " \"help\": {\n", + " \"@id\": \"help\",\n", + " \"aliases\": [\n", + " \"h\"\n", + " ],\n", + " \"default\": false,\n", + " \"description\": \"Show additional help to know more about the possible parameters\",\n", + " \"options\": [\n", + " true,\n", + " false\n", + " ],\n", + " \"required\": true\n", + " },\n", + " \"in-headers\": {\n", + " \"aliases\": [\n", + " \"headers\",\n", + " \"inheaders\",\n", + " \"inHeaders\",\n", + " \"in-headers\",\n", + " \"in_headers\"\n", + " ],\n", + " \"default\": false,\n", + " \"description\": \"Only include the JSON-LD context in the headers\",\n", + " \"options\": [\n", + " true,\n", + " false\n", + " ],\n", + " \"required\": true\n", + " },\n", + " \"informat\": {\n", + " \"@id\": \"informat\",\n", + " \"aliases\": [\n", + " \"f\"\n", + " ],\n", + " \"default\": \"text\",\n", + " \"description\": \"input format\",\n", + " \"options\": [\n", + " \"text\",\n", + " \"json-ld\"\n", + " ],\n", + " \"required\": false\n", + " },\n", + " \"input\": {\n", + " \"@id\": \"input\",\n", + " \"aliases\": [\n", + " \"i\"\n", + " ],\n", + " \"help\": \"Input text\",\n", + " \"required\": true\n", + " },\n", + " \"intype\": {\n", + " \"@id\": \"intype\",\n", + " \"aliases\": [\n", + " \"t\"\n", + " ],\n", + " \"default\": \"direct\",\n", + " \"description\": \"input type\",\n", + " \"options\": [\n", + " \"direct\",\n", + " \"url\",\n", + " \"file\"\n", + " ],\n", + " \"required\": false\n", + " },\n", + " \"language\": {\n", + " \"aliases\": [\n", + " \"language\",\n", + " \"l\"\n", + " ],\n", + " \"default\": \"en\",\n", + " \"description\": \"language of the input\",\n", + " \"options\": [\n", + " \"es\",\n", + " \"en\"\n", + " ],\n", + " \"required\": true\n", + " },\n", + " \"outformat\": {\n", + " \"@id\": \"outformat\",\n", + " \"aliases\": [\n", + " \"o\"\n", + " ],\n", + " \"default\": \"json-ld\",\n", + " \"description\": \"The data can be semantically formatted (JSON-LD, turtle or n-triples),\\ngiven as a list of comma-separated fields (see the fields option) or constructed from a Jinja2\\ntemplate (see the template option).\",\n", + " \"options\": [\n", + " \"json-ld\",\n", + " \"turtle\",\n", + " \"ntriples\"\n", + " ],\n", + " \"required\": true\n", + " },\n", + " \"prefix\": {\n", + " \"@id\": \"prefix\",\n", + " \"aliases\": [\n", + " \"p\"\n", + " ],\n", + " \"default\": \"\",\n", + " \"description\": \"prefix to use for new entities\",\n", + " \"required\": true\n", + " },\n", + " \"template\": {\n", + " \"@id\": \"template\",\n", + " \"description\": \"Jinja2 template for the result. The input data for the template will\\nbe the results as a dictionary.\\nFor example:\\n\\nConsider the results before templating:\\n\\n```\\n[{\\n \\\"@type\\\": \\\"entry\\\",\\n \\\"onyx:hasEmotionSet\\\": [],\\n \\\"nif:isString\\\": \\\"testing the template\\\",\\n \\\"marl:hasOpinion\\\": [\\n {\\n \\\"@type\\\": \\\"sentiment\\\",\\n \\\"marl:hasPolarity\\\": \\\"marl:Positive\\\"\\n }\\n ]\\n}]\\n```\\n\\n\\nAnd the template:\\n\\n```\\n{% for entry in entries %}\\n{{ entry[\\\"nif:isString\\\"] | upper }},{{entry.sentiments[0][\\\"marl:hasPolarity\\\"].split(\\\":\\\")[1]}}\\n{% endfor %}\\n```\\n\\nThe final result would be:\\n\\n```\\nTESTING THE TEMPLATE,Positive\\n```\\n\",\n", + " \"required\": false\n", + " },\n", + " \"urischeme\": {\n", + " \"@id\": \"urischeme\",\n", + " \"aliases\": [\n", + " \"u\"\n", + " ],\n", + " \"default\": \"RFC5147String\",\n", + " \"description\": \"scheme for NIF URIs\",\n", + " \"options\": [\n", + " \"RFC5147String\"\n", + " ],\n", + " \"required\": false\n", + " },\n", + " \"verbose\": {\n", + " \"@id\": \"verbose\",\n", + " \"aliases\": [\n", + " \"v\"\n", + " ],\n", + " \"default\": false,\n", + " \"description\": \"Show all properties in the result\",\n", + " \"options\": [\n", + " true,\n", + " false\n", + " ],\n", + " \"required\": true\n", + " },\n", + " \"with-parameters\": {\n", + " \"aliases\": [\n", + " \"withparameters\",\n", + " \"with_parameters\"\n", + " ],\n", + " \"default\": false,\n", + " \"description\": \"include initial parameters in the response\",\n", + " \"options\": [\n", + " true,\n", + " false\n", + " ],\n", + " \"required\": true\n", + " }\n", + " }\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import requests\n", + "res = requests.get(f'{endpoint}/',\n", + " params={\n", + " \"help\": True}).text\n", + "pretty(res)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ignoring the context" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
{\n",
+       "  "@type": "Results",\n",
+       "  "entries": [\n",
+       "    {\n",
+       "      "@id": "prefix:",\n",
+       "      "@type": "Entry",\n",
+       "      "marl:hasOpinion": [],\n",
+       "      "nif:isString": "This will tell senpy to only include the context in the headers",\n",
+       "      "onyx:hasEmotionSet": [\n",
+       "        {\n",
+       "          "@id": "Emotions0",\n",
+       "          "@type": "EmotionSet",\n",
+       "          "onyx:hasEmotion": [\n",
+       "            {\n",
+       "              "@id": "Emotion0",\n",
+       "              "@type": "Emotion",\n",
+       "              "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal": 4.22,\n",
+       "              "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance": 5.17,\n",
+       "              "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence": 5.2,\n",
+       "              "prov:wasGeneratedBy": "prefix:Analysis_1554364689.0180304"\n",
+       "            }\n",
+       "          ],\n",
+       "          "prov:wasGeneratedBy": "prefix:Analysis_1554364689.0180304"\n",
+       "        }\n",
+       "      ]\n",
+       "    }\n",
+       "  ]\n",
+       "}\n",
+       "
\n" + ], + "text/latex": [ + "\\begin{Verbatim}[commandchars=\\\\\\{\\}]\n", + "\\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Results\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}entries\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Entry\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}marl:hasOpinion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}nif:isString\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}This will tell senpy to only include the context in the headers\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotionSet\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotions0\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}EmotionSet\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}onyx:hasEmotion\\PYZdq{}}\\PY{p}{:} \\PY{p}{[}\n", + " \\PY{p}{\\PYZob{}}\n", + " \\PY{n+nd}{\\PYZdq{}@id\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion0\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nd}{\\PYZdq{}@type\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}Emotion\\PYZdq{}}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns\\PYZsh{}arousal\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{4.22}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns\\PYZsh{}dominance\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{5.17}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns\\PYZsh{}valence\\PYZdq{}}\\PY{p}{:} \\PY{l+m+mf}{5.2}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364689.0180304\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\\PY{p}{,}\n", + " \\PY{n+nt}{\\PYZdq{}prov:wasGeneratedBy\\PYZdq{}}\\PY{p}{:} \\PY{l+s+s2}{\\PYZdq{}prefix:Analysis\\PYZus{}1554364689.0180304\\PYZdq{}}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + " \\PY{p}{\\PYZcb{}}\n", + " \\PY{p}{]}\n", + "\\PY{p}{\\PYZcb{}}\n", + "\\end{Verbatim}\n" + ], + "text/plain": [ + "{\n", + " \"@type\": \"Results\",\n", + " \"entries\": [\n", + " {\n", + " \"@id\": \"prefix:\",\n", + " \"@type\": \"Entry\",\n", + " \"marl:hasOpinion\": [],\n", + " \"nif:isString\": \"This will tell senpy to only include the context in the headers\",\n", + " \"onyx:hasEmotionSet\": [\n", + " {\n", + " \"@id\": \"Emotions0\",\n", + " \"@type\": \"EmotionSet\",\n", + " \"onyx:hasEmotion\": [\n", + " {\n", + " \"@id\": \"Emotion0\",\n", + " \"@type\": \"Emotion\",\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal\": 4.22,\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance\": 5.17,\n", + " \"http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence\": 5.2,\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364689.0180304\"\n", + " }\n", + " ],\n", + " \"prov:wasGeneratedBy\": \"prefix:Analysis_1554364689.0180304\"\n", + " }\n", + " ]\n", + " }\n", + " ]\n", + "}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import requests\n", + "res = requests.get(f'{endpoint}/',\n", + " params={\n", + " \"input\": \"This will tell senpy to only include the context in the headers\",\n", + " \"inheaders\": True})\n", + "pretty(res.text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To retrieve the context URI, use the `LINK` header:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ";rel=\"http://www.w3.org/ns/json-ld#context\"; type=\"application/ld+json\"\n" + ] + } + ], + "source": [ + "print(res.headers['Link'])" + ] + } + ], + "metadata": { + "anaconda-cloud": {}, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "toc": { + "colors": { + "hover_highlight": "#DAA520", + "running_highlight": "#FF0000", + "selected_highlight": "#FFD700" + }, + "moveMenuLeft": true, + "nav_menu": { + "height": "68px", + "width": "252px" + }, + "navigate_menu": true, + "number_sections": true, + "sideBar": true, + "threshold": 4, + "toc_cell": false, + "toc_section_display": "block", + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/docs/Quickstart.rst b/docs/Quickstart.rst new file mode 100644 index 0000000..4e401bb --- /dev/null +++ b/docs/Quickstart.rst @@ -0,0 +1,2599 @@ + +Consuming Senpy services +======================== + +This short tutorial will teach you how to consume services in several +ways, taking advantage of the features of the framework. + +In particular, we will cover: + +- Annotating text with sentiment +- Annotating text with emotion +- Getting results in different formats (Turtle, XML, text…) +- Asking for specific emotion models (automatic model conversion) +- Listing available services in an endpoint +- Switching to different services +- Calling multiple services in the same request (Pipelines) + +The latest version of this IPython notebook is available at: +https://github.com/gsi-upm/senpy/tree/master/docs/Quickstart.ipynb + +Requirements +------------ + +For the sake of simplicity, this tutorial will use the demo server: +http://senpy.gsi.upm.es: + +.. code:: ipython3 + + endpoint = 'http://senpy.gsi.upm.es/api' + +This server runs some open source plugins for sentiment and emotion +analysis. + +The HTTP API of Senpy can be queried with your favourite tool. This is +just an example using curl: + +.. code:: bash + + curl "http://senpy.gsi.upm.es/api/sentiment140" --data-urlencode "input=Senpy is awesome" + +For simplicity, in this tutorial we will use the requests library. We +will also add a function to add syntax highlighting for the +JSON-LD/Turtle results: + +.. code:: ipython3 + + try: + from IPython.display import Code + def pretty(txt, language='json-ld'): + return Code(txt, language=language) + except ImportError: + def pretty(txt, **kwargs): + print(txt) + +Once you’re familiar with Senpy, you can deploy your own instance quite +easily. e.g. using docker: + +:: + + docker run -ti --name 'SenpyEndpoint' -d -p 5000:5000 gsiupm/senpy + +Then, feel free to change the endpoint variable to run the examples in +your own instance. + +Sentiment Analysis of Text +-------------------------- + +To start, let us analyse the sentiment in the following sentence: *senpy +is a wonderful service*. + +For now, we will use the `sentiment140 `__ +service, through the sentiment140 plugin. We will later cover how to use +a different service. + +.. code:: ipython3 + + import requests + res = requests.get(f'{endpoint}/sentiment140', + params={"input": "Senpy is awesome",}) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lIw%3D%3D",
+      "@type": "Results",
+      "entries": [
+        {
+          "@id": "prefix:",
+          "@type": "Entry",
+          "marl:hasOpinion": [
+            {
+              "@type": "Sentiment",
+              "marl:hasPolarity": "marl:Positive",
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364667.7955277"
+            }
+          ],
+          "nif:isString": "Senpy is awesome",
+          "onyx:hasEmotionSet": []
+        }
+      ]
+    }
+    
+ + + + +Senpy services always return an object of type ``senpy:Results``, with a +list of entries. You can think of an entry as a self-contained textual +context (``nif:Context`` and ``senpy:Entry``). Entries can be as short +as a sentence, or as long as a news article. + +Each entry has a ``nif:isString`` property that contains the original +text of the entry, and several other properties that are provided by the +plugins. + +For instance, sentiment annotations are provided through +``marl:hasOpinion``. + +The annotations are semantic. We can ask Senpy for the expanded JSON-LD +output to reveal the full URIs of each property and entity: + +.. code:: ipython3 + + import requests + res = requests.get(f'{endpoint}/sentiment140', + params={"input": "Senpy is awesome", + "expanded": True}) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lJmV4cGFuZGVkPVRydWUj",
+      "@type": [
+        "http://www.gsi.upm.es/onto/senpy/ns#Results"
+      ],
+      "http://www.w3.org/ns/prov#used": [
+        {
+          "@id": "http://senpy.invalid/",
+          "@type": [
+            "http://www.gsi.upm.es/onto/senpy/ns#Entry"
+          ],
+          "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#isString": [
+            {
+              "@value": "Senpy is awesome"
+            }
+          ],
+          "http://www.gsi.dit.upm.es/ontologies/marl/ns#hasOpinion": [
+            {
+              "@type": [
+                "http://www.gsi.upm.es/onto/senpy/ns#Sentiment"
+              ],
+              "http://www.gsi.dit.upm.es/ontologies/marl/ns#hasPolarity": [
+                {
+                  "@value": "marl:Positive"
+                }
+              ],
+              "http://www.w3.org/ns/prov#wasGeneratedBy": [
+                {
+                  "@id": "http://senpy.invalid/Analysis_1554364668.1011338"
+                }
+              ]
+            }
+          ],
+          "http://www.gsi.dit.upm.es/ontologies/onyx/ns#hasEmotionSet": []
+        }
+      ]
+    }
+    
+ + + + +.. code:: ipython3 + + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lJmV4cGFuZGVkPVRydWUj",
+      "@type": [
+        "http://www.gsi.upm.es/onto/senpy/ns#Results"
+      ],
+      "http://www.w3.org/ns/prov#used": [
+        {
+          "@id": "http://senpy.invalid/",
+          "@type": [
+            "http://www.gsi.upm.es/onto/senpy/ns#Entry"
+          ],
+          "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#isString": [
+            {
+              "@value": "Senpy is awesome"
+            }
+          ],
+          "http://www.gsi.dit.upm.es/ontologies/marl/ns#hasOpinion": [
+            {
+              "@type": [
+                "http://www.gsi.upm.es/onto/senpy/ns#Sentiment"
+              ],
+              "http://www.gsi.dit.upm.es/ontologies/marl/ns#hasPolarity": [
+                {
+                  "@value": "marl:Positive"
+                }
+              ],
+              "http://www.w3.org/ns/prov#wasGeneratedBy": [
+                {
+                  "@id": "http://senpy.invalid/Analysis_1554364668.1011338"
+                }
+              ]
+            }
+          ],
+          "http://www.gsi.dit.upm.es/ontologies/onyx/ns#hasEmotionSet": []
+        }
+      ]
+    }
+    
+ + + + +Other output formats +-------------------- + +Senpy supports several semantic formats, like turtle and xml-RDF. You +can select the format of the output with the ``outformat`` parameter: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/sentiment140', + params={"input": "Senpy is the best framework for semantic sentiment analysis, and very easy to use", + "outformat": "turtle"}) + pretty(res.text, language='turtle') + + + + +.. raw:: html + +
@prefix : <http://www.gsi.upm.es/onto/senpy/ns#> .
+    @prefix dc: <http://dublincore.org/2012/06/14/dcelements#> .
+    @prefix emoml: <http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/emotionml/ns#> .
+    @prefix endpoint: <http://senpy.gsi.upm.es/api/> .
+    @prefix fam: <http://vocab.fusepool.info/fam#> .
+    @prefix marl: <http://www.gsi.dit.upm.es/ontologies/marl/ns#> .
+    @prefix nif: <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#> .
+    @prefix onyx: <http://www.gsi.dit.upm.es/ontologies/onyx/ns#> .
+    @prefix prefix: <http://senpy.invalid/> .
+    @prefix prov: <http://www.w3.org/ns/prov#> .
+    @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+    @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+    @prefix senpy: <http://www.gsi.upm.es/onto/senpy/ns#> .
+    @prefix wna: <http://www.gsi.dit.upm.es/ontologies/wnaffect/ns#> .
+    @prefix xml: <http://www.w3.org/XML/1998/namespace> .
+    @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+    
+    prefix: a senpy:Entry ;
+        nif:isString "Senpy is the best framework for semantic sentiment analysis, and very easy to use" ;
+        marl:hasOpinion [ a senpy:Sentiment ;
+                marl:hasPolarity "marl:Positive" ;
+                prov:wasGeneratedBy prefix:Analysis_1554364668.5153766 ] .
+    
+    [] a senpy:Results ;
+        prov:used prefix: .
+    
+ + + + +Selecting fields from the output +-------------------------------- + +The full output in the previous sections is very useful because it is +semantically annotated. However, it is also quite verbose if we only +want to label a piece of text, or get a polarity value. + +For such simple cases, the API has a special ``fields`` method you can +use to get a specific field from the results, and even transform the +results. Senpy uses jmespath under the hood, which has its own notation. + +To illustrate this, let us get only the text (``nif:isString``) from +each entry: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/sentiment140', + params={"input": "Senpy is a wonderful service", + "fields": 'entries[]."nif:isString"'}) + print(res.text) + + +.. parsed-literal:: + + ["Senpy is a wonderful service"] + + +Or we could get both the text and the polarity of the text (assuming +there is only one opinion per entry) with a slightly more complicated +query: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/sentiment140', + params={"input": "Senpy is a service. Wonderful service.", + "delimiter": "sentence", + "fields": 'entries[0].["nif:isString", "marl:hasOpinion"[0]."marl:hasPolarity"]'}) + print(res.text) + + +.. parsed-literal:: + + ["Senpy is a service. Wonderful service.", "marl:Neutral"] + + +jmespath is rather extensive for this tutorial. We will cover only the +most simple cases, so you do not need to learn much about the notation. + +For more complicated transformations, check out +`jmespath `__. In addition to a fairly complete +documentation, they have a live environment you can use to test your +queries. + +Emotion analysis +---------------- + +Senpy uses the ``onyx`` vocabulary to represent emotions, which +incorporates the notion of ``EmotionSet``\ ’s, an emotion that is +composed of several emotions. In a nutshell, an ``Entry`` is linked to +one or more ``EmotionSet``, which in turn is made up of one or more +``Emotion``. + +Let’s illustrate it with an example, using the ``emotion-depechemood`` +plugin. + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/emotion-depechemood', + params={"input": "Senpy is a wonderful that service"}) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tZGVwZWNoZW1vb2Q_aW5wdXQ9U2VucHkraXMrYSt3b25kZXJmdWwrdGhhdCtzZXJ2aWNlIw%3D%3D",
+      "@type": "Results",
+      "entries": [
+        {
+          "@id": "prefix:",
+          "@type": "Entry",
+          "marl:hasOpinion": [],
+          "nif:isString": "Senpy is a wonderful that service",
+          "onyx:hasEmotionSet": [
+            {
+              "@type": "EmotionSet",
+              "onyx:hasEmotion": [
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:negative-fear",
+                  "onyx:hasEmotionIntensity": 0.06258366271018097
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:amusement",
+                  "onyx:hasEmotionIntensity": 0.15784834034155437
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:anger",
+                  "onyx:hasEmotionIntensity": 0.08728815135373413
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:annoyance",
+                  "onyx:hasEmotionIntensity": 0.12184635680460143
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:indifference",
+                  "onyx:hasEmotionIntensity": 0.1374081151031531
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:joy",
+                  "onyx:hasEmotionIntensity": 0.12267040802346799
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:awe",
+                  "onyx:hasEmotionIntensity": 0.21085262130713067
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:sadness",
+                  "onyx:hasEmotionIntensity": 0.09950234435617733
+                }
+              ],
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364674.7078097"
+            }
+          ]
+        }
+      ]
+    }
+    
+ + + + +As you have probably noticed, there are several emotions in this result, +each with a different intensity. + +We can also tell senpy to only return the emotion with the maximum +intensity using the ``maxemotion`` parameter: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/emotion-depechemood', + params={"input": "Senpy is a wonderful service", + "maxemotion": True}) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tZGVwZWNoZW1vb2Q_aW5wdXQ9U2VucHkraXMrYSt3b25kZXJmdWwrc2VydmljZSZtYXhlbW90aW9uPVRydWUj",
+      "@type": "Results",
+      "entries": [
+        {
+          "@id": "prefix:",
+          "@type": "Entry",
+          "marl:hasOpinion": [],
+          "nif:isString": "Senpy is a wonderful service",
+          "onyx:hasEmotionSet": [
+            {
+              "@type": "EmotionSet",
+              "onyx:hasEmotion": [
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:awe",
+                  "onyx:hasEmotionIntensity": 0.21085262130713067
+                }
+              ],
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364674.8374224"
+            }
+          ]
+        }
+      ]
+    }
+    
+ + + + +We can combine this feature with the ``fields`` parameter to get only +the label and the intensity: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/emotion-depechemood', + params={"input": "Senpy is a wonderful service", + "fields": 'entries[]."onyx:hasEmotionSet"[]."onyx:hasEmotion"[]["onyx:hasEmotionCategory","onyx:hasEmotionIntensity"]', + "maxemotion": True}) + pretty(res.text) + + + + +.. raw:: html + +
[["wna:awe", 0.21085262130713067]]
+    
+ + + + +Emotion conversion +------------------ + +If the model used by a plugin is not right for your application, you can +ask for a specific emotion model in your request. + +Senpy ships with emotion conversion capabilities, and it will try to +automatically convert the results. + +For example, the ``emotion-anew`` plugin uses the dimensional ``pad`` +(or VAD, valence-arousal-dominance) model, as we can see here: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/emotion-anew', + params={"input": "Senpy is a wonderful service and I love it"}) + print(res.text) + + +.. parsed-literal:: + + { + "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQj", + "@type": "Results", + "entries": [ + { + "@id": "prefix:", + "@type": "Entry", + "marl:hasOpinion": [], + "nif:isString": "Senpy is a wonderful service and I love it", + "onyx:hasEmotionSet": [ + { + "@id": "Emotions0", + "@type": "EmotionSet", + "onyx:hasEmotion": [ + { + "@id": "Emotion0", + "@type": "Emotion", + "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal": 6.44, + "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance": 7.11, + "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence": 8.72, + "prov:wasGeneratedBy": "prefix:Analysis_1554364675.1427004" + } + ], + "prov:wasGeneratedBy": "prefix:Analysis_1554364675.1427004" + } + ] + } + ] + } + + +If we need a category level, we can ask for the equivalent results in +the ``big6`` model: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/emotion-anew', + params={"input": "Senpy is a wonderful service and I love it", + "emotion-model": "emoml:big6"}) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQmZW1vdGlvbi1tb2RlbD1lbW9tbCUzQWJpZzYj",
+      "@type": "Results",
+      "entries": [
+        {
+          "@id": "prefix:",
+          "@type": "Entry",
+          "marl:hasOpinion": [],
+          "nif:isString": "Senpy is a wonderful service and I love it",
+          "onyx:hasEmotionSet": [
+            {
+              "@id": "Emotions0",
+              "@type": "EmotionSet",
+              "onyx:hasEmotion": [
+                {
+                  "@id": "Emotion0",
+                  "@type": "Emotion",
+                  "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal": 6.44,
+                  "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance": 7.11,
+                  "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence": 8.72,
+                  "prov:wasGeneratedBy": "prefix:Analysis_1554364675.2834926"
+                }
+              ],
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364675.2834926"
+            },
+            {
+              "@type": "EmotionSet",
+              "onyx:hasEmotion": [
+                {
+                  "@type": "Emotion",
+                  "onyx:algorithmConfidence": 7.449999999999999,
+                  "onyx:hasEmotionCategory": "emoml:big6fear"
+                }
+              ],
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364675.2902758"
+            }
+          ]
+        }
+      ]
+    }
+    
+ + + + +Because we don’t usually care about the original emotion, the conversion +can be presented in three ways: + +- full: the original and converted emotions are included at the same + level +- filtered: the original emotion is replaced by the converted emotion +- nested: the original emotion is replaced, but the converted emotion + points to it + +For example, here’s how the ``nested`` structure would look like: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/emotion-anew', + params={"input": "Senpy is a wonderful service and I love it", + "emotion-model": "emoml:big6", + "conversion": "nested"}) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2Vtb3Rpb24tYW5ldz9pbnB1dD1TZW5weStpcythK3dvbmRlcmZ1bCtzZXJ2aWNlK2FuZCtJK2xvdmUraXQmZW1vdGlvbi1tb2RlbD1lbW9tbCUzQWJpZzYmY29udmVyc2lvbj1uZXN0ZWQj",
+      "@type": "Results",
+      "entries": [
+        {
+          "@id": "prefix:",
+          "@type": "Entry",
+          "marl:hasOpinion": [],
+          "nif:isString": "Senpy is a wonderful service and I love it",
+          "onyx:hasEmotionSet": [
+            {
+              "@type": "EmotionSet",
+              "onyx:hasEmotion": [
+                {
+                  "@type": "Emotion",
+                  "onyx:algorithmConfidence": 7.449999999999999,
+                  "onyx:hasEmotionCategory": "emoml:big6fear"
+                }
+              ],
+              "prov:wasDerivedFrom": {
+                "@id": "Emotions0",
+                "@type": "EmotionSet",
+                "onyx:hasEmotion": [
+                  {
+                    "@id": "Emotion0",
+                    "@type": "Emotion",
+                    "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal": 6.44,
+                    "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance": 7.11,
+                    "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence": 8.72,
+                    "prov:wasGeneratedBy": "prefix:Analysis_1554364675.4125388"
+                  }
+                ],
+                "prov:wasGeneratedBy": "prefix:Analysis_1554364675.4125388"
+              },
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364675.4143574"
+            }
+          ]
+        }
+      ]
+    }
+    
+ + + + +Again, for completion, we could get only the label with the ``fields`` +parameter: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/emotion-anew', + params={"input": "Senpy is a wonderful service and I love it", + "emotion-model": "emoml:big6", + "fields": 'entries[].[["nif:isString","onyx:hasEmotionSet"[]."onyx:hasEmotion"[]."onyx:hasEmotionCategory"][]][]', + "conversion": "filtered"}) + pretty(res.text) + + + + +.. raw:: html + +
[["Senpy is a wonderful service and I love it", "emoml:big6fear"]]
+    
+ + + + +Built-in client +--------------- + +The built-in senpy client allows you to query any Senpy endpoint. We +will illustrate how to use it with the public demo endpoint, and then +show you how to spin up your own endpoint using docker. + +Building pipelines +------------------ + +You can query several senpy services in the same request. This feature +is called pipelining, and the result of combining several plugins in a +request is called a pipeline. + +The simplest way to use pipelines is to add every plugin you want to use +to the URL, separated by either a slash or a comma. + +For instance, to get sentiment (``sentiment140``) and emotion +(``depechemood``) annotations at the same time: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/sentiment140/emotion-depechemood', + params={"input": "Senpy is a wonderful service"}) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MC9lbW90aW9uLWRlcGVjaGVtb29kP2lucHV0PVNlbnB5K2lzK2Erd29uZGVyZnVsK3NlcnZpY2Uj",
+      "@type": "Results",
+      "entries": [
+        {
+          "@id": "prefix:",
+          "@type": "Entry",
+          "marl:hasOpinion": [
+            {
+              "@type": "Sentiment",
+              "marl:hasPolarity": "marl:Neutral",
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364675.8928602"
+            }
+          ],
+          "nif:isString": "Senpy is a wonderful service",
+          "onyx:hasEmotionSet": [
+            {
+              "@type": "EmotionSet",
+              "onyx:hasEmotion": [
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:negative-fear",
+                  "onyx:hasEmotionIntensity": 0.06258366271018097
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:amusement",
+                  "onyx:hasEmotionIntensity": 0.15784834034155437
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:anger",
+                  "onyx:hasEmotionIntensity": 0.08728815135373413
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:annoyance",
+                  "onyx:hasEmotionIntensity": 0.12184635680460143
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:indifference",
+                  "onyx:hasEmotionIntensity": 0.1374081151031531
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:joy",
+                  "onyx:hasEmotionIntensity": 0.12267040802346799
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:awe",
+                  "onyx:hasEmotionIntensity": 0.21085262130713067
+                },
+                {
+                  "@type": "Emotion",
+                  "onyx:hasEmotionCategory": "wna:sadness",
+                  "onyx:hasEmotionIntensity": 0.09950234435617733
+                }
+              ],
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364675.8937423"
+            }
+          ]
+        }
+      ]
+    }
+    
+ + + + +In a senpy pipeline, the call is processed by each plugin in sequence. +The output of a plugin is used as input for the next one. + +Pipelines take the same parameters as the plugins they are made of. For +example, if we want to split the original sentence before analysing its +sentiment, we can use a pipeline made out of the ``split`` and the +``sentiment140`` plugins. + +``split`` takes an extra parameter (``delimiter``) to select the type of +splitting (by sentence or by paragraph), and ``sentiment140`` takes a +``language`` parameter. + +This is how the request looks like: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/split/sentiment140', + params={"input": "Senpy is awesome. And services are composable.", + "delimiter": "sentence", + "language": "en", + "outformat": "json-ld"}) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NwbGl0L3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcythd2Vzb21lLitBbmQrc2VydmljZXMrYXJlK2NvbXBvc2FibGUuJmRlbGltaXRlcj1zZW50ZW5jZSZsYW5ndWFnZT1lbiZvdXRmb3JtYXQ9anNvbi1sZCM%3D",
+      "@type": "Results",
+      "entries": [
+        {
+          "@id": "prefix:",
+          "@type": "Entry",
+          "marl:hasOpinion": [
+            {
+              "@type": "Sentiment",
+              "marl:hasPolarity": "marl:Positive",
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364676.2060485"
+            }
+          ],
+          "nif:isString": "Senpy is awesome. And services are composable.",
+          "onyx:hasEmotionSet": []
+        },
+        {
+          "@id": "prefix:#char=0,17",
+          "@type": "Entry",
+          "marl:hasOpinion": [
+            {
+              "@type": "Sentiment",
+              "marl:hasPolarity": "marl:Positive",
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364676.2060485"
+            }
+          ],
+          "nif:isString": "Senpy is awesome.",
+          "onyx:hasEmotionSet": []
+        },
+        {
+          "@id": "prefix:#char=18,46",
+          "@type": "Entry",
+          "marl:hasOpinion": [
+            {
+              "@type": "Sentiment",
+              "marl:hasPolarity": "marl:Neutral",
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364676.2060485"
+            }
+          ],
+          "nif:isString": "And services are composable.",
+          "onyx:hasEmotionSet": []
+        }
+      ]
+    }
+    
+ + + + +As you can see, ``split`` creates two new entries, which are also +annotated by ``sentiment140``. + +Once again, we could use the ``fields`` parameter to get a list of +strings and labels: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/split/sentiment140', + params={"input": "Senpy is awesome. And services are composable.", + "delimiter": "sentence", + "fields": 'entries[].[["nif:isString","marl:hasOpinion"[]."marl:hasPolarity"][]][]', + "language": "en", + "outformat": "json-ld"}) + pretty(res.text) + + + + +.. raw:: html + +
[["Senpy is awesome. And services are composable.", "marl:Positive"], ["Senpy is awesome.", "marl:Positive"], ["And services are composable.", "marl:Neutral"]]
+    
+ + + + +Evaluation +---------- + +Sentiment analysis plugins can also be evaluated on a series of +pre-defined datasets, using the ``gsitk`` tool. + +For instance, to evaluate the ``sentiment-vader`` plugin on the +``vader`` and ``sts`` datasets, we would simply call: + +.. code:: ipython3 + + res = requests.get(f'{endpoint}/evaluate', + params={"algo": "sentiment-vader", + "dataset": "vader,sts", + 'outformat': 'json-ld' + }) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL2V2YWx1YXRlLz9hbGdvPXNlbnRpbWVudC12YWRlciZkYXRhc2V0PXZhZGVyJTJDc3RzJm91dGZvcm1hdD1qc29uLWxkIw%3D%3D",
+      "@type": "AggregatedEvaluation",
+      "senpy:evaluations": [
+        {
+          "@type": "Evaluation",
+          "evaluates": "endpoint:plugins/sentiment-vader_0.1.1__vader",
+          "evaluatesOn": "vader",
+          "metrics": [
+            {
+              "@type": "Accuracy",
+              "value": 0.6907142857142857
+            },
+            {
+              "@type": "Precision_macro",
+              "value": 0.34535714285714286
+            },
+            {
+              "@type": "Recall_macro",
+              "value": 0.5
+            },
+            {
+              "@type": "F1_macro",
+              "value": 0.40853400929446554
+            },
+            {
+              "@type": "F1_weighted",
+              "value": 0.5643605528396403
+            },
+            {
+              "@type": "F1_micro",
+              "value": 0.6907142857142857
+            },
+            {
+              "@type": "F1_macro",
+              "value": 0.40853400929446554
+            }
+          ]
+        },
+        {
+          "@type": "Evaluation",
+          "evaluates": "endpoint:plugins/sentiment-vader_0.1.1__sts",
+          "evaluatesOn": "sts",
+          "metrics": [
+            {
+              "@type": "Accuracy",
+              "value": 0.3107177974434612
+            },
+            {
+              "@type": "Precision_macro",
+              "value": 0.1553588987217306
+            },
+            {
+              "@type": "Recall_macro",
+              "value": 0.5
+            },
+            {
+              "@type": "F1_macro",
+              "value": 0.23705926481620407
+            },
+            {
+              "@type": "F1_weighted",
+              "value": 0.14731706525451424
+            },
+            {
+              "@type": "F1_micro",
+              "value": 0.3107177974434612
+            },
+            {
+              "@type": "F1_macro",
+              "value": 0.23705926481620407
+            }
+          ]
+        }
+      ]
+    }
+    
+ + + + +The same results can be visualized as a table in the Web interface: + +|image0| + +.. |image0| image:: evaluation-results.png + +**note**: to evaluate a plugin on a dataset, senpy will need to predict +the labels of the entries using the plugin. This process might take long +for plugins that use an external service, such as ``sentiment140``. + +Advanced topics +--------------- + +Verbose output +~~~~~~~~~~~~~~ + +By default, senpy does not include information that might be too +verbose, such as the parameters that were used in the analysis. + +You can instruct senpy to provide a more verbose output with the +``verbose`` parameter: + +.. code:: ipython3 + + import requests + res = requests.get(f'{endpoint}/sentiment140', + params={ + "input": "Senpy is the best framework for semantic sentiment analysis, and very easy to use", + "verbose": True}).text + pretty(res) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpL3NlbnRpbWVudDE0MD9pbnB1dD1TZW5weStpcyt0aGUrYmVzdCtmcmFtZXdvcmsrZm9yK3NlbWFudGljK3NlbnRpbWVudCthbmFseXNpcyUyQythbmQrdmVyeStlYXN5K3RvK3VzZSZ2ZXJib3NlPVRydWUj",
+      "@type": "Results",
+      "activities": [
+        {
+          "@id": "prefix:Analysis_1554364688.7944896",
+          "@type": "Analysis",
+          "marl:maxPolarityValue": 1,
+          "marl:minPolarityValue": 0,
+          "prov:used": [
+            {
+              "@type": "Parameter",
+              "name": "input",
+              "value": "Senpy is the best framework for semantic sentiment analysis, and very easy to use"
+            },
+            {
+              "@type": "Parameter",
+              "name": "verbose",
+              "value": true
+            },
+            {
+              "@type": "Parameter",
+              "name": "in-headers",
+              "value": false
+            },
+            {
+              "@type": "Parameter",
+              "name": "algorithm",
+              "value": "default"
+            },
+            {
+              "@type": "Parameter",
+              "name": "expanded-jsonld",
+              "value": false
+            },
+            {
+              "@type": "Parameter",
+              "name": "with-parameters",
+              "value": false
+            },
+            {
+              "@type": "Parameter",
+              "name": "outformat",
+              "value": "json-ld"
+            },
+            {
+              "@type": "Parameter",
+              "name": "help",
+              "value": false
+            },
+            {
+              "@type": "Parameter",
+              "name": "aliases",
+              "value": false
+            },
+            {
+              "@type": "Parameter",
+              "name": "conversion",
+              "value": "full"
+            },
+            {
+              "@type": "Parameter",
+              "name": "intype",
+              "value": "direct"
+            },
+            {
+              "@type": "Parameter",
+              "name": "informat",
+              "value": "text"
+            },
+            {
+              "@type": "Parameter",
+              "name": "prefix",
+              "value": ""
+            },
+            {
+              "@type": "Parameter",
+              "name": "urischeme",
+              "value": "RFC5147String"
+            },
+            {
+              "@type": "Parameter",
+              "name": "language",
+              "value": "auto"
+            }
+          ],
+          "prov:wasAssociatedWith": "endpoint:plugins/sentiment140_0.2"
+        }
+      ],
+      "entries": [
+        {
+          "@id": "prefix:",
+          "@type": "Entry",
+          "marl:hasOpinion": [
+            {
+              "@type": "Sentiment",
+              "marl:hasPolarity": "marl:Positive",
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364688.7944896"
+            }
+          ],
+          "nif:isString": "Senpy is the best framework for semantic sentiment analysis, and very easy to use",
+          "onyx:hasEmotionSet": []
+        }
+      ]
+    }
+    
+ + + + +Getting help +~~~~~~~~~~~~ + +.. code:: ipython3 + + import requests + res = requests.get(f'{endpoint}/', + params={ + "help": True}).text + pretty(res) + + + + +.. raw:: html + +
{
+      "@context": "http://senpy.gsi.upm.es/api/contexts/YXBpLz9oZWxwPVRydWUj",
+      "@type": "Help",
+      "valid_parameters": {
+        "algorithm": {
+          "aliases": [
+            "algorithms",
+            "a",
+            "algo"
+          ],
+          "default": "default",
+          "description": "Algorithms that will be used to process the request.It may be a list of comma-separated names.",
+          "processor": "string_to_tuple",
+          "required": true
+        },
+        "aliases": {
+          "@id": "aliases",
+          "aliases": [],
+          "default": false,
+          "description": "Replace JSON properties with their aliases",
+          "options": [
+            true,
+            false
+          ],
+          "required": true
+        },
+        "conversion": {
+          "@id": "conversion",
+          "default": "full",
+          "description": "How to show the elements that have (not) been converted.\n\n* full: converted and original elements will appear side-by-side\n* filtered: only converted elements will be shown\n* nested: converted elements will be shown, and they will include a link to the original element\n(using `prov:wasGeneratedBy`).\n",
+          "options": [
+            "filtered",
+            "nested",
+            "full"
+          ],
+          "required": true
+        },
+        "emotion-model": {
+          "@id": "emotionModel",
+          "aliases": [
+            "emoModel",
+            "emotionModel"
+          ],
+          "description": "Emotion model to use in the response.\nSenpy will try to convert the output to this model automatically.\n\nExamples: `wna:liking` and `emoml:big6`.\n        ",
+          "required": false
+        },
+        "expanded-jsonld": {
+          "@id": "expanded-jsonld",
+          "aliases": [
+            "expanded",
+            "expanded_jsonld"
+          ],
+          "default": false,
+          "description": "use JSON-LD expansion to get full URIs",
+          "options": [
+            true,
+            false
+          ],
+          "required": true
+        },
+        "fields": {
+          "@id": "fields",
+          "description": "A jmespath selector, that can be used to extract a new dictionary, array or value\nfrom the results.\njmespath is a powerful query language for json and/or dictionaries.\nIt allows you to change the structure (and data) of your objects through queries.\n\ne.g., the following expression gets a list of `[emotion label, intensity]` for each entry:\n`entries[].\"onyx:hasEmotionSet\"[].\"onyx:hasEmotion\"[][\"onyx:hasEmotionCategory\",\"onyx:hasEmotionIntensity\"]`\n\nFor more information, see: https://jmespath.org\n\n",
+          "required": false
+        },
+        "help": {
+          "@id": "help",
+          "aliases": [
+            "h"
+          ],
+          "default": false,
+          "description": "Show additional help to know more about the possible parameters",
+          "options": [
+            true,
+            false
+          ],
+          "required": true
+        },
+        "in-headers": {
+          "aliases": [
+            "headers",
+            "inheaders",
+            "inHeaders",
+            "in-headers",
+            "in_headers"
+          ],
+          "default": false,
+          "description": "Only include the JSON-LD context in the headers",
+          "options": [
+            true,
+            false
+          ],
+          "required": true
+        },
+        "informat": {
+          "@id": "informat",
+          "aliases": [
+            "f"
+          ],
+          "default": "text",
+          "description": "input format",
+          "options": [
+            "text",
+            "json-ld"
+          ],
+          "required": false
+        },
+        "input": {
+          "@id": "input",
+          "aliases": [
+            "i"
+          ],
+          "help": "Input text",
+          "required": true
+        },
+        "intype": {
+          "@id": "intype",
+          "aliases": [
+            "t"
+          ],
+          "default": "direct",
+          "description": "input type",
+          "options": [
+            "direct",
+            "url",
+            "file"
+          ],
+          "required": false
+        },
+        "language": {
+          "aliases": [
+            "language",
+            "l"
+          ],
+          "default": "en",
+          "description": "language of the input",
+          "options": [
+            "es",
+            "en"
+          ],
+          "required": true
+        },
+        "outformat": {
+          "@id": "outformat",
+          "aliases": [
+            "o"
+          ],
+          "default": "json-ld",
+          "description": "The data can be semantically formatted (JSON-LD, turtle or n-triples),\ngiven as a list of comma-separated fields (see the fields option) or constructed from a Jinja2\ntemplate (see the template option).",
+          "options": [
+            "json-ld",
+            "turtle",
+            "ntriples"
+          ],
+          "required": true
+        },
+        "prefix": {
+          "@id": "prefix",
+          "aliases": [
+            "p"
+          ],
+          "default": "",
+          "description": "prefix to use for new entities",
+          "required": true
+        },
+        "template": {
+          "@id": "template",
+          "description": "Jinja2 template for the result. The input data for the template will\nbe the results as a dictionary.\nFor example:\n\nConsider the results before templating:\n\n```\n[{\n    \"@type\": \"entry\",\n    \"onyx:hasEmotionSet\": [],\n    \"nif:isString\": \"testing the template\",\n    \"marl:hasOpinion\": [\n        {\n            \"@type\": \"sentiment\",\n            \"marl:hasPolarity\": \"marl:Positive\"\n        }\n    ]\n}]\n```\n\n\nAnd the template:\n\n```\n{% for entry in entries %}\n{{ entry[\"nif:isString\"] | upper }},{{entry.sentiments[0][\"marl:hasPolarity\"].split(\":\")[1]}}\n{% endfor %}\n```\n\nThe final result would be:\n\n```\nTESTING THE TEMPLATE,Positive\n```\n",
+          "required": false
+        },
+        "urischeme": {
+          "@id": "urischeme",
+          "aliases": [
+            "u"
+          ],
+          "default": "RFC5147String",
+          "description": "scheme for NIF URIs",
+          "options": [
+            "RFC5147String"
+          ],
+          "required": false
+        },
+        "verbose": {
+          "@id": "verbose",
+          "aliases": [
+            "v"
+          ],
+          "default": false,
+          "description": "Show all properties in the result",
+          "options": [
+            true,
+            false
+          ],
+          "required": true
+        },
+        "with-parameters": {
+          "aliases": [
+            "withparameters",
+            "with_parameters"
+          ],
+          "default": false,
+          "description": "include initial parameters in the response",
+          "options": [
+            true,
+            false
+          ],
+          "required": true
+        }
+      }
+    }
+    
+ + + + +Ignoring the context +~~~~~~~~~~~~~~~~~~~~ + +.. code:: ipython3 + + import requests + res = requests.get(f'{endpoint}/', + params={ + "input": "This will tell senpy to only include the context in the headers", + "inheaders": True}) + pretty(res.text) + + + + +.. raw:: html + +
{
+      "@type": "Results",
+      "entries": [
+        {
+          "@id": "prefix:",
+          "@type": "Entry",
+          "marl:hasOpinion": [],
+          "nif:isString": "This will tell senpy to only include the context in the headers",
+          "onyx:hasEmotionSet": [
+            {
+              "@id": "Emotions0",
+              "@type": "EmotionSet",
+              "onyx:hasEmotion": [
+                {
+                  "@id": "Emotion0",
+                  "@type": "Emotion",
+                  "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#arousal": 4.22,
+                  "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#dominance": 5.17,
+                  "http://www.gsi.dit.upm.es/ontologies/onyx/vocabularies/anew/ns#valence": 5.2,
+                  "prov:wasGeneratedBy": "prefix:Analysis_1554364689.0180304"
+                }
+              ],
+              "prov:wasGeneratedBy": "prefix:Analysis_1554364689.0180304"
+            }
+          ]
+        }
+      ]
+    }
+    
+ + + + +To retrieve the context URI, use the ``LINK`` header: + +.. code:: ipython3 + + print(res.headers['Link']) + + +.. parsed-literal:: + + ;rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json" + diff --git a/docs/SenpyClientUse.ipynb b/docs/SenpyClientUse.ipynb deleted file mode 100644 index 30edfbe..0000000 --- a/docs/SenpyClientUse.ipynb +++ /dev/null @@ -1,317 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:05:31.465571Z", - "start_time": "2017-04-10T19:05:31.458282+02:00" - }, - "deletable": true, - "editable": true - }, - "source": [ - "# Client" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true, - "deletable": true, - "editable": true - }, - "source": [ - "The built-in senpy client allows you to query any Senpy endpoint. We will illustrate how to use it with the public demo endpoint, and then show you how to spin up your own endpoint using docker." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "Demo Endpoint\n", - "-------------" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "To start using senpy, simply create a new Client and point it to your endpoint. In this case, the latest version of Senpy at GSI." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:29:12.827640Z", - "start_time": "2017-04-10T19:29:12.818617+02:00" - }, - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "from senpy.client import Client\n", - "\n", - "c = Client('http://latest.senpy.cluster.gsi.dit.upm.es/api')\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "Now, let's use that client analyse some queries:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:29:14.011657Z", - "start_time": "2017-04-10T19:29:13.701808+02:00" - }, - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "r = c.analyse('I like sugar!!', algorithm='sentiment140')\n", - "r" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:08:19.616754Z", - "start_time": "2017-04-10T19:08:19.610767+02:00" - }, - "deletable": true, - "editable": true - }, - "source": [ - "As you can see, that gave us the full JSON result. A more concise way to print it would be:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:29:14.854213Z", - "start_time": "2017-04-10T19:29:14.842068+02:00" - }, - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "for entry in r.entries:\n", - " print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "We can also obtain a list of available plugins with the client:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:29:16.245198Z", - "start_time": "2017-04-10T19:29:16.056545+02:00" - }, - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "c.plugins()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "Or, more concisely:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:29:17.663275Z", - "start_time": "2017-04-10T19:29:17.484623+02:00" - }, - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "c.plugins().keys()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "Local Endpoint\n", - "--------------" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "To run your own instance of senpy, just create a docker container with the latest Senpy image. Using `--default-plugins` you will get some extra plugins to start playing with the API." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:29:20.637539Z", - "start_time": "2017-04-10T19:29:19.938322+02:00" - }, - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "!docker run -ti --name 'SenpyEndpoint' -d -p 6000:5000 gsiupm/senpy:0.8.6 --host 0.0.0.0 --default-plugins" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "To use this endpoint:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:29:21.263976Z", - "start_time": "2017-04-10T19:29:21.260595+02:00" - }, - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "c_local = Client('http://127.0.0.1:6000/api')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "deletable": true, - "editable": true - }, - "source": [ - "That's all! After you are done with your analysis, stop the docker container:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2017-04-10T17:29:33.226686Z", - "start_time": "2017-04-10T19:29:22.392121+02:00" - }, - "collapsed": false, - "deletable": true, - "editable": true - }, - "outputs": [], - "source": [ - "!docker stop SenpyEndpoint\n", - "!docker rm SenpyEndpoint" - ] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.0" - }, - "toc": { - "colors": { - "hover_highlight": "#DAA520", - "running_highlight": "#FF0000", - "selected_highlight": "#FFD700" - }, - "moveMenuLeft": true, - "nav_menu": { - "height": "68px", - "width": "252px" - }, - "navigate_menu": true, - "number_sections": true, - "sideBar": true, - "threshold": 4, - "toc_cell": false, - "toc_section_display": "block", - "toc_window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/docs/SenpyClientUse.rst b/docs/SenpyClientUse.rst deleted file mode 100644 index 892eb58..0000000 --- a/docs/SenpyClientUse.rst +++ /dev/null @@ -1,106 +0,0 @@ - -Client -====== - -Demo Endpoint -------------- - -Import Client and send a request - -.. code:: python - - from senpy.client import Client - - c = Client('http://latest.senpy.cluster.gsi.dit.upm.es/api') - r = c.analyse('I like Pizza', algorithm='sentiment140') - -Print response - -.. code:: python - - for entry in r.entries: - print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity'])) - - -.. parsed-literal:: - - I like Pizza -> marl:Positive - - -Obtain a list of available plugins - -.. code:: python - - for plugin in c.request('/plugins')['plugins']: - print(plugin['name']) - - -.. parsed-literal:: - - emoRand - rand - sentiment140 - - -Local Endpoint --------------- - -Run a docker container with Senpy image and default plugins - -.. code:: - - docker run -ti --name 'SenpyEndpoint' -d -p 5000:5000 gsiupm/senpy:0.8.6 --host 0.0.0.0 --default-plugins - - -.. parsed-literal:: - - a0157cd98057072388bfebeed78a830da7cf0a796f4f1a3fd9188f9f2e5fe562 - - -Import client and send a request to localhost - -.. code:: python - - c_local = Client('http://127.0.0.1:5000/api') - r = c_local.analyse('Hello world', algorithm='sentiment140') - -Print response - -.. code:: python - - for entry in r.entries: - print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity'])) - - -.. parsed-literal:: - - Hello world -> marl:Neutral - - -Obtain a list of available plugins deployed locally - -.. code:: python - - c_local.plugins().keys() - - -.. parsed-literal:: - - rand - sentiment140 - emoRand - - -Stop the docker container - -.. code:: python - - !docker stop SenpyEndpoint - !docker rm SenpyEndpoint - - -.. parsed-literal:: - - SenpyEndpoint - SenpyEndpoint - diff --git a/docs/about.rst b/docs/about.rst deleted file mode 100644 index b457ef6..0000000 --- a/docs/about.rst +++ /dev/null @@ -1,11 +0,0 @@ -About --------- - -If you use Senpy in your research, please cite `Senpy: A Pragmatic Linked Sentiment Analysis Framework `__ (`BibTex `__): - -.. code-block:: text - - Sánchez-Rada, J. F., Iglesias, C. A., Corcuera, I., & Araque, Ó. (2016, October). - Senpy: A Pragmatic Linked Sentiment Analysis Framework. - In Data Science and Advanced Analytics (DSAA), - 2016 IEEE International Conference on (pp. 735-742). IEEE. diff --git a/docs/advanced.rst b/docs/advanced.rst new file mode 100644 index 0000000..b6665c6 --- /dev/null +++ b/docs/advanced.rst @@ -0,0 +1,9 @@ +Advanced usage +-------------- + +.. toctree:: + :maxdepth: 1 + + server-cli + conversion + commandline \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst index ed90f3c..1f3e74b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -25,7 +25,7 @@ NIF API "@context":"http://127.0.0.1/api/contexts/Results.jsonld", "@id":"_:Results_11241245.22", "@type":"results" - "analysis": [ + "activities": [ "plugins/sentiment-140_0.1" ], "entries": [ @@ -73,7 +73,7 @@ NIF API .. http:get:: /api/plugins Returns a list of installed plugins. - **Example request**: + **Example request and response**: .. sourcecode:: http @@ -82,10 +82,6 @@ NIF API Accept: application/json, text/javascript - **Example response**: - - .. sourcecode:: http - { "@id": "plugins/sentiment-140_0.1", "@type": "sentimentPlugin", @@ -143,19 +139,14 @@ NIF API .. http:get:: /api/plugins/ Returns the information of a specific plugin. - **Example request**: + **Example request and response**: .. sourcecode:: http - GET /api/plugins/rand/ HTTP/1.1 + GET /api/plugins/sentiment-random/ HTTP/1.1 Host: localhost Accept: application/json, text/javascript - - **Example response**: - - .. sourcecode:: http - { "@context": "http://127.0.0.1/api/contexts/ExamplePlugin.jsonld", "@id": "plugins/ExamplePlugin_0.1", diff --git a/docs/apischema.rst b/docs/apischema.rst index 01b6a86..75a129a 100644 --- a/docs/apischema.rst +++ b/docs/apischema.rst @@ -1,5 +1,6 @@ -API and Examples -################ +API and vocabularies +#################### + .. toctree:: vocabularies.rst diff --git a/docs/bad-examples/results/example-analysis-as-id-FAIL.json b/docs/bad-examples/results/example-analysis-as-id-FAIL.json index f3dcc5b..700c884 100644 --- a/docs/bad-examples/results/example-analysis-as-id-FAIL.json +++ b/docs/bad-examples/results/example-analysis-as-id-FAIL.json @@ -2,7 +2,7 @@ "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", "@type": "results", - "analysis": [ + "activities": [ "me:SAnalysis1", "me:SgAnalysis1", "me:EmotionAnalysis1", diff --git a/docs/bad-examples/results/example-basic-FAIL.json b/docs/bad-examples/results/example-basic-FAIL.json index 606a0df..abf6d3e 100644 --- a/docs/bad-examples/results/example-basic-FAIL.json +++ b/docs/bad-examples/results/example-basic-FAIL.json @@ -2,7 +2,7 @@ "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "http://example.com#NIFExample", "@type": "results", - "analysis": [ + "activities": [ ], "entries": [ { diff --git a/docs/commandline.rst b/docs/commandline.rst index 903fcc5..47582a7 100644 --- a/docs/commandline.rst +++ b/docs/commandline.rst @@ -1,7 +1,8 @@ Command line ============ -This video shows how to analyse text directly on the command line using the senpy tool. +Although the main use of senpy is to publish services, the tool can also be used locally to analyze text in the command line. +This is a short video demonstration: .. image:: https://asciinema.org/a/9uwef1ghkjk062cw2t4mhzpyk.png :width: 100% diff --git a/docs/conversion.rst b/docs/conversion.rst index b6c64d5..c761148 100644 --- a/docs/conversion.rst +++ b/docs/conversion.rst @@ -7,9 +7,9 @@ Senpy includes experimental support for emotion/sentiment conversion plugins. Use === -Consider the original query: http://127.0.0.1:5000/api/?i=hello&algo=emoRand +Consider the original query: http://127.0.0.1:5000/api/?i=hello&algo=emotion-random -The requested plugin (emoRand) returns emotions using Ekman's model (or big6 in EmotionML): +The requested plugin (emotion-random) returns emotions using Ekman's model (or big6 in EmotionML): .. code:: json @@ -21,14 +21,14 @@ The requested plugin (emoRand) returns emotions using Ekman's model (or big6 in "@type": "emotion", "onyx:hasEmotionCategory": "emoml:big6anger" }, - "prov:wasGeneratedBy": "plugins/emoRand_0.1" + "prov:wasGeneratedBy": "plugins/emotion-random_0.1" } To get these emotions in VAD space (FSRE dimensions in EmotionML), we'd do this: -http://127.0.0.1:5000/api/?i=hello&algo=emoRand&emotionModel=emoml:fsre-dimensions +http://127.0.0.1:5000/api/?i=hello&algo=emotion-random&emotionModel=emoml:fsre-dimensions This call, provided there is a valid conversion plugin from Ekman's to VAD, would return something like this: @@ -42,7 +42,7 @@ This call, provided there is a valid conversion plugin from Ekman's to VAD, woul "@type": "emotion", "onyx:hasEmotionCategory": "emoml:big6anger" }, - "prov:wasGeneratedBy": "plugins/emoRand_0.1" + "prov:wasGeneratedBy": "plugins/emotion-random.1" }, { "@type": "emotionSet", "onyx:hasEmotion": { @@ -69,7 +69,7 @@ It is also possible to get the original emotion nested within the new converted "@type": "emotion", "onyx:hasEmotionCategory": "emoml:big6anger" }, - "prov:wasGeneratedBy": "plugins/emoRand_0.1" + "prov:wasGeneratedBy": "plugins/emotion-random.1" "onyx:wasDerivedFrom": { "@type": "emotionSet", "onyx:hasEmotion": { diff --git a/docs/demo.rst b/docs/demo.rst index f25aa37..5e32e55 100644 --- a/docs/demo.rst +++ b/docs/demo.rst @@ -1,7 +1,7 @@ Demo ---- -There is a demo available on http://senpy.cluster.gsi.dit.upm.es/, where you can test a serie of different plugins. +There is a demo available on http://senpy.gsi.upm.es/, where you can test a live instance of Senpy, with several open source plugins. You can use the playground (a web interface) or make HTTP requests to the service API. .. image:: senpy-playground.png @@ -10,7 +10,5 @@ You can use the playground (a web interface) or make HTTP requests to the servic :scale: 100 % :align: center -Plugins Demo -============ -The source code and description of the plugins used in the demo is available here: https://lab.cluster.gsi.dit.upm.es/senpy/senpy-plugins-community/. +The source code and description of the plugins used in the demo are available here: https://lab.gsi.upm.es/senpy/senpy-plugins-community/. diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 0000000..221bdcc --- /dev/null +++ b/docs/development.rst @@ -0,0 +1,27 @@ +Developing new services +----------------------- + +Developing web services can be hard. +To illustrate it, the figure below summarizes the typical features in a text analysis service. + +.. image:: senpy-framework.png + :width: 60% + :align: center + +Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems. +Among other things, Senpy takes care of these tasks: + + * Interfacing with the user: parameter validation, error handling. + * Formatting: JSON-LD, Turtle/n-triples input and output, or simple text input + * Linked Data: senpy results are semantically annotated, using a series of well established vocabularies, and sane default URIs. + * User interface: a web UI where users can explore your service and test different settings + * A client to interact with the service. Currently only available in Python. + +You only need to provide the algorithm to turn a piece of text into an annotation +Sharing your sentiment analysis with the world has never been easier! + +.. toctree:: + :maxdepth: 1 + + plugins-quickstart + plugins-faq \ No newline at end of file diff --git a/docs/evaluation-results.png b/docs/evaluation-results.png new file mode 100644 index 0000000..7372394 Binary files /dev/null and b/docs/evaluation-results.png differ diff --git a/docs/examples.rst b/docs/examples.rst index 9c1456f..a00d171 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -1,5 +1,6 @@ Examples ------- +-------- + All the examples in this page use the :download:`the main schema <_static/schemas/definitions.json>`. Simple NIF annotation @@ -17,6 +18,7 @@ Sentiment Analysis ..................... Description ,,,,,,,,,,, + This annotation corresponds to the sentiment analysis of an input. The example shows the sentiment represented according to Marl format. The sentiments detected are contained in the Sentiments array with their related part of the text. diff --git a/docs/examples/results/example-analysis-as-id.json b/docs/examples/results/example-analysis-as-id.json index 4b1dfb0..63f60ca 100644 --- a/docs/examples/results/example-analysis-as-id.json +++ b/docs/examples/results/example-analysis-as-id.json @@ -2,7 +2,7 @@ "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", "@type": "results", - "analysis": [ + "activities": [ { "@id": "_:SAnalysis1_Activity", "@type": "marl:SentimentAnalysis", diff --git a/docs/examples/results/example-basic.json b/docs/examples/results/example-basic.json index e78c2e5..ae14753 100644 --- a/docs/examples/results/example-basic.json +++ b/docs/examples/results/example-basic.json @@ -2,7 +2,7 @@ "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", "@type": "results", - "analysis": [ ], + "activities": [ ], "entries": [ { "@id": "http://example.org#char=0,40", diff --git a/docs/examples/results/example-complete.json b/docs/examples/results/example-complete.json index dcdebd0..477df19 100644 --- a/docs/examples/results/example-complete.json +++ b/docs/examples/results/example-complete.json @@ -2,7 +2,7 @@ "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", "@type": "results", - "analysis": [ + "activities": [ { "@id": "_:SAnalysis1_Activity", "@type": "marl:SentimentAnalysis", diff --git a/docs/examples/results/example-emotion.json b/docs/examples/results/example-emotion.json index 5eaa8d3..9547302 100644 --- a/docs/examples/results/example-emotion.json +++ b/docs/examples/results/example-emotion.json @@ -2,7 +2,7 @@ "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", "@type": "results", - "analysis": [ + "activities": [ { "@id": "me:EmotionAnalysis1_Activity", "@type": "me:EmotionAnalysis1", diff --git a/docs/examples/results/example-ner.json b/docs/examples/results/example-ner.json index 39215ce..107f74f 100644 --- a/docs/examples/results/example-ner.json +++ b/docs/examples/results/example-ner.json @@ -2,7 +2,7 @@ "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", "@type": "results", - "analysis": [ + "activities": [ { "@id": "_:NER1_Activity", "@type": "me:NERAnalysis", diff --git a/docs/examples/results/example-pad.json b/docs/examples/results/example-pad.json index d092f55..63beac8 100644 --- a/docs/examples/results/example-pad.json +++ b/docs/examples/results/example-pad.json @@ -7,7 +7,7 @@ ], "@id": "me:Result1", "@type": "results", - "analysis": [ + "activities": [ { "@id": "me:HesamsAnalysis_Activity", "@type": "onyx:EmotionAnalysis", diff --git a/docs/examples/results/example-sentiment.json b/docs/examples/results/example-sentiment.json index 318c55c..4fe910b 100644 --- a/docs/examples/results/example-sentiment.json +++ b/docs/examples/results/example-sentiment.json @@ -2,7 +2,7 @@ "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", "@type": "results", - "analysis": [ + "activities": [ { "@id": "_:SAnalysis1_Activity", "@type": "marl:SentimentAnalysis", diff --git a/docs/examples/results/example-suggestion.json b/docs/examples/results/example-suggestion.json index 2d6903e..b267f46 100644 --- a/docs/examples/results/example-suggestion.json +++ b/docs/examples/results/example-suggestion.json @@ -2,7 +2,7 @@ "@context": "http://mixedemotions-project.eu/ns/context.jsonld", "@id": "me:Result1", "@type": "results", - "analysis": [ + "activities": [ { "@id": "_:SgAnalysis1_Activity", "@type": "me:SuggestionAnalysis", diff --git a/docs/index.rst b/docs/index.rst index 245c34d..21a9bc0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,32 +4,31 @@ Welcome to Senpy's documentation! :target: http://senpy.readthedocs.io/en/latest/ .. image:: https://badge.fury.io/py/senpy.svg :target: https://badge.fury.io/py/senpy -.. image:: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/badges/master/build.svg - :target: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/commits/master -.. image:: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/badges/master/coverage.svg - :target: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/commits/master +.. image:: https://lab.gsi.upm.es/senpy/senpy/badges/master/build.svg + :target: https://lab.gsi.upm.es/senpy/senpy/commits/master +.. image:: https://lab.gsi.upm.es/senpy/senpy/badges/master/coverage.svg + :target: https://lab.gsi.upm.es/senpy/senpy/commits/master .. image:: https://img.shields.io/pypi/l/requests.svg - :target: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/ + :target: https://lab.gsi.upm.es/senpy/senpy/ Senpy is a framework for sentiment and emotion analysis services. -Services built with senpy are interchangeable and easy to use because they share a common :doc:`apischema`. -It also simplifies service development. +Senpy services are interchangeable and easy to use because they share a common semantic :doc:`apischema`. -.. image:: senpy-architecture.png - :width: 100% - :align: center +If you interested in consuming Senpy services, read :doc:`Quickstart`. +To get familiar with the concepts behind Senpy, and what it can offer for service developers, check out :doc:`development`. +:doc:`apischema` contains information about the semantic models and vocabularies used by Senpy. .. toctree:: :caption: Learn more about senpy: :maxdepth: 2 senpy + Quickstart installation - demo - usage + development apischema - plugins - conversion - about + advanced + demo + publications diff --git a/docs/installation.rst b/docs/installation.rst index 64b98bc..e81d1f6 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -32,27 +32,25 @@ If you want to install senpy globally, use sudo instead of the ``--user`` flag. Docker Image ************ -Build the image or use the pre-built one: +The base image of senpy comes with some builtin plugins that you can use: .. code:: bash - docker run -ti -p 5000:5000 gsiupm/senpy --host 0.0.0.0 --default-plugins + docker run -ti -p 5000:5000 gsiupm/senpy --host 0.0.0.0 -To add custom plugins, use a docker volume: +To add your custom plugins, you can use a docker volume: .. code:: bash - docker run -ti -p 5000:5000 -v :/plugins gsiupm/senpy --host 0.0.0.0 --default-plugins -f /plugins + docker run -ti -p 5000:5000 -v :/plugins gsiupm/senpy --host 0.0.0.0 --plugins -f /plugins -Python 2 -........ -There is a Senpy version for python2 too: +There is a Senpy image for **python 2**, too: .. code:: bash - docker run -ti -p 5000:5000 gsiupm/senpy:python2.7 --host 0.0.0.0 --default-plugins + docker run -ti -p 5000:5000 gsiupm/senpy:python2.7 --host 0.0.0.0 Alias @@ -62,7 +60,7 @@ If you are using the docker approach regularly, it is advisable to use a script .. code:: bash - alias senpy='docker run --rm -ti -p 5000:5000 -v $PWD:/senpy-plugins gsiupm/senpy --default-plugins' + alias senpy='docker run --rm -ti -p 5000:5000 -v $PWD:/senpy-plugins gsiupm/senpy' Now, you may run senpy from any folder in your computer like so: diff --git a/docs/plugins-definition.rst b/docs/plugins-definition.rst index 706195f..bec02a8 100644 --- a/docs/plugins-definition.rst +++ b/docs/plugins-definition.rst @@ -110,4 +110,4 @@ Now, in a file named ``helloworld.py``: entry.sentiments.append(sentiment) yield entry -The complete code of the example plugin is available `here `__. +The complete code of the example plugin is available `here `__. diff --git a/docs/plugins.rst b/docs/plugins-faq.rst similarity index 73% rename from docs/plugins.rst rename to docs/plugins-faq.rst index e56ecec..7ddb192 100644 --- a/docs/plugins.rst +++ b/docs/plugins-faq.rst @@ -1,61 +1,18 @@ -Developing new plugins ----------------------- -This document contains the minimum to get you started with developing new analysis plugin. -For an example of conversion plugins, see :doc:`conversion`. -For a description of definition files, see :doc:`plugins-definition`. - -A more step-by-step tutorial with slides is available `here `__ +F.A.Q. +====== .. contents:: :local: -What is a plugin? -================= - -A plugin is a python object that can process entries. Given an entry, it will modify it, add annotations to it, or generate new entries. - - -What is an entry? -================= - -Entries are objects that can be annotated. -In general, they will be a piece of text. -By default, entries are `NIF contexts `_ represented in JSON-LD format. -It is a dictionary/JSON object that looks like this: - - .. code:: python - - { - "@id": "", - "nif:isString": "input text", - "sentiments": [ { - ... - } - ], - ... - } - -Annotations are added to the object like this: - -.. code:: python - - entry = Entry() - entry.vocabulary__annotationName = 'myvalue' - entry['vocabulary:annotationName'] = 'myvalue' - entry['annotationNameURI'] = 'myvalue' - -Where vocabulary is one of the prefixes defined in the default senpy context, and annotationURI is a full URI. -The value may be any valid JSON-LD dictionary. -For simplicity, senpy includes a series of models by default in the ``senpy.models`` module. What are annotations? -===================== +##################### They are objects just like entries. Senpy ships with several default annotations, including: ``Sentiment``, ``Emotion``, ``EmotionSet``...jk bb What's a plugin made of? -======================== +######################## When receiving a query, senpy selects what plugin or plugins should process each entry, and in what order. It also makes sure the every entry and the parameters provided by the user meet the plugin requirements. @@ -73,37 +30,25 @@ In practice, this is what a plugin looks like, tests included: The lines highlighted contain some information about the plugin. In particular, the following information is mandatory: -* A unique name for the class. In our example, Rand. +* A unique name for the class. In our example, sentiment-random. * The subclass/type of plugin. This is typically either `SentimentPlugin` or `EmotionPlugin`. However, new types of plugin can be created for different annotations. The only requirement is that these new types inherit from `senpy.Analysis` * A description of the plugin. This can be done simply by adding a doc to the class. * A version, which should get updated. * An author name. -Plugins Code -============ - -The basic methods in a plugin are: - -* analyse_entry: called in every user requests. It takes two parameters: ``Entry``, the entry object, and ``params``, the parameters supplied by the user. It should yield one or more ``Entry`` objects. -* activate: used to load memory-hungry resources. For instance, to train a classifier. -* deactivate: used to free up resources when the plugin is no longer needed. - -Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method. - - How does senpy find modules? -============================ +############################ Senpy looks for files of two types: * Python files of the form `senpy_.py` or `_plugin.py`. In these files, it will look for: 1) Instances that inherit from `senpy.Plugin`, or subclasses of `senpy.Plugin` that can be initialized without a configuration file. i.e. classes that contain all the required attributes for a plugin. -* Plugin definition files (see :doc:`advanced-plugins`) +* Plugin definition files (see :doc:`plugins-definition`) -Defining additional parameters -============================== +How can I define additional parameters for my plugin? +##################################################### -Your plugin may ask for additional parameters from the users of the service by using the attribute ``extra_params`` in your plugin definition. +Your plugin may ask for additional parameters from users by using the attribute ``extra_params`` in your plugin definition. It takes a dictionary, where the keys are the name of the argument/parameter, and the value has the following fields: * aliases: the different names which can be used in the request to use the parameter. @@ -124,8 +69,8 @@ It takes a dictionary, where the keys are the name of the argument/parameter, an -Loading data and files -====================== +How should I load external data and files +######################################### Most plugins will need access to files (dictionaries, lexicons, etc.). These files are usually heavy or under a license that does not allow redistribution. @@ -144,7 +89,7 @@ Plugins have a convenience function `self.open` which will automatically prepend file_in_data = file_in_sources = - def activate(self): + def on activate(self): with self.open(self.file_in_data) as f: self._classifier = train_from_file(f) file_in_source = os.path.join(self.get_folder(), self.file_in_sources) @@ -155,8 +100,8 @@ Plugins have a convenience function `self.open` which will automatically prepend It is good practice to specify the paths of these files in the plugin configuration, so the same code can be reused with different resources. -Docker image -============ +Can I build a docker image for my plugin? +######################################### Add the following dockerfile to your project to generate a docker image with your plugin: @@ -204,17 +149,15 @@ Adding data to the image: FROM gsiupm/senpy:1.0.1 COPY data / -F.A.Q. -====== What annotations can I use? -??????????????????????????? +########################### You can add almost any annotation to an entry. The most common use cases are covered in the :doc:`apischema`. Why does the analyse function yield instead of return? -?????????????????????????????????????????????????????? +###################################################### This is so that plugins may add new entries to the response or filter some of them. For instance, a chunker may split one entry into several. @@ -222,7 +165,7 @@ On the other hand, a conversion plugin may leave out those entries that do not c If I'm using a classifier, where should I train it? -??????????????????????????????????????????????????? +################################################### Training a classifier can be time time consuming. To avoid running the training unnecessarily, you can use ShelfMixin to store the classifier. For instance: @@ -256,7 +199,7 @@ A corrupt shelf prevents the plugin from loading. If you do not care about the data in the shelf, you can force your plugin to remove the corrupted file and load anyway, set the 'force_shelf' to True in your plugin and start it again. How can I turn an external service into a plugin? -????????????????????????????????????????????????? +################################################# This example ilustrate how to implement a plugin that accesses the Sentiment140 service. @@ -292,8 +235,8 @@ This example ilustrate how to implement a plugin that accesses the Sentiment140 yield entry -Can I activate a DEBUG mode for my plugin? -??????????????????????????????????????????? +How can I activate a DEBUG mode for my plugin? +############################################### You can activate the DEBUG mode by the command-line tool using the option -d. @@ -309,6 +252,6 @@ Additionally, with the ``--pdb`` option you will be dropped into a pdb post mort python -m pdb yourplugin.py Where can I find more code examples? -???????????????????????????????????? +#################################### See: ``_. diff --git a/docs/plugins-quickstart.rst b/docs/plugins-quickstart.rst new file mode 100644 index 0000000..a55a4f3 --- /dev/null +++ b/docs/plugins-quickstart.rst @@ -0,0 +1,86 @@ +Quickstart for service developers +================================= + +This document contains the minimum to get you started with developing new services using Senpy. + +For an example of conversion plugins, see :doc:`conversion`. +For a description of definition files, see :doc:`plugins-definition`. + +A more step-by-step tutorial with slides is available `here `__ + +.. contents:: :local: + +Installation +############ + +First of all, you need to install the package. +See :doc:`installation` for instructions. +Once installed, the `senpy` command should be available. + +Architecture +############ + +The main component of a sentiment analysis service is the algorithm itself. However, for the algorithm to work, it needs to get the appropriate parameters from the user, format the results according to the defined API, interact with the user whn errors occur or more information is needed, etc. + +Senpy proposes a modular and dynamic architecture that allows: + +* Implementing different algorithms in a extensible way, yet offering a common interface. +* Offering common services that facilitate development, so developers can focus on implementing new and better algorithms. + +The framework consists of two main modules: Senpy core, which is the building block of the service, and Senpy plugins, which consist of the analysis algorithm. The next figure depicts a simplified version of the processes involved in an analysis with the Senpy framework. + +.. image:: senpy-architecture.png + :width: 100% + :align: center + + +What is a plugin? +################# + +A plugin is a python object that can process entries. Given an entry, it will modify it, add annotations to it, or generate new entries. + + +What is an entry? +################# + +Entries are objects that can be annotated. +In general, they will be a piece of text. +By default, entries are `NIF contexts `_ represented in JSON-LD format. +It is a dictionary/JSON object that looks like this: + + .. code:: python + + { + "@id": "", + "nif:isString": "input text", + "sentiments": [ { + ... + } + ], + ... + } + +Annotations are added to the object like this: + +.. code:: python + + entry = Entry() + entry.vocabulary__annotationName = 'myvalue' + entry['vocabulary:annotationName'] = 'myvalue' + entry['annotationNameURI'] = 'myvalue' + +Where vocabulary is one of the prefixes defined in the default senpy context, and annotationURI is a full URI. +The value may be any valid JSON-LD dictionary. +For simplicity, senpy includes a series of models by default in the ``senpy.models`` module. + +Plugins Code +############ + +The basic methods in a plugin are: + +* analyse_entry: called in every user requests. It takes two parameters: ``Entry``, the entry object, and ``params``, the parameters supplied by the user. It should yield one or more ``Entry`` objects. +* activate: used to load memory-hungry resources. For instance, to train a classifier. +* deactivate: used to free up resources when the plugin is no longer needed. + +Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method. + diff --git a/docs/publications.rst b/docs/publications.rst new file mode 100644 index 0000000..edd646c --- /dev/null +++ b/docs/publications.rst @@ -0,0 +1,46 @@ +Publications +============ + + +If you use Senpy in your research, please cite `Senpy: A Pragmatic Linked Sentiment Analysis Framework `__ (`BibTex `__): + +.. code-block:: text + + Sánchez-Rada, J. F., Iglesias, C. A., Corcuera, I., & Araque, Ó. (2016, October). + Senpy: A Pragmatic Linked Sentiment Analysis Framework. + In Data Science and Advanced Analytics (DSAA), + 2016 IEEE International Conference on (pp. 735-742). IEEE. + + + +Senpy uses Onyx for emotion representation, first introduced in: + +.. code-block:: text + + Sánchez-Rada, J. F., & Iglesias, C. A. (2016). + Onyx: A linked data approach to emotion representation. + Information Processing & Management, 52(1), 99-114. + +Senpy uses Marl for sentiment representation, which was presented in: + +.. code-block:: text + + Westerski, A., Iglesias Fernandez, C. A., & Tapia Rico, F. (2011). + Linked opinions: Describing sentiments on the structured web of data. + + +Senpy has been used extensively in the toolbox of the MixedEmotions project: + +.. code-block:: text + + Buitelaar, P., Wood, I. D., Arcan, M., McCrae, J. P., Abele, A., Robin, C., … Tummarello, G. (2018). + MixedEmotions: An Open-Source Toolbox for Multi-Modal Emotion Analysis. + IEEE Transactions on Multimedia. + +The representation models, formats and challenges are partially covered in a chapter of the book Sentiment Analysis in Social Networks: + +.. code-block:: text + + Iglesias, C. A., Sánchez-Rada, J. F., Vulcu, G., & Buitelaar, P. (2017). + Linked Data Models for Sentiment and Emotion Analysis in Social Networks. + In Sentiment Analysis in Social Networks (pp. 49-69). \ No newline at end of file diff --git a/docs/senpy.rst b/docs/senpy.rst index bbdfb0d..1e385a9 100644 --- a/docs/senpy.rst +++ b/docs/senpy.rst @@ -1,54 +1,27 @@ What is Senpy? -------------- -Senpy is a framework for text analysis using Linked Data. There are three main applications of Senpy so far: sentiment and emotion analysis, user profiling and entity recoginition. Annotations and Services are compliant with NIF (NLP Interchange Format). +Senpy is a framework for sentiment and emotion analysis services. +Its goal is to produce analysis services that are interchangeable and fully interoperable. -Senpy aims at providing a framework where analysis modules can be integrated easily as plugins, and providing a core functionality for managing tasks such as data validation, user interaction, formatting, logging, translation to linked data, etc. - -The figure below summarizes the typical features in a text analysis service. -Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems. - -.. image:: senpy-framework.png - :width: 60% +.. image:: senpy-architecture.png + :width: 100% :align: center - -Senpy for end users -=================== - All services built using senpy share a common interface. -This allows users to use them (almost) interchangeably. -Senpy comes with a :ref:`built-in client`. - - -Senpy for service developers -============================ +This allows users to use them (almost) interchangeably, with the same API and tools, simply by pointing to a different URL or changing a parameter. +The common schema also makes it easier to evaluate the performance of different algorithms and services. +In fact, Senpy has a built-in evaluation API you can use to compare results with different algorithms. -Senpy is a framework that turns your sentiment or emotion analysis algorithm into a full blown semantic service. -Senpy takes care of: +Services can also use the common interface to communicate with each other. +And higher level features can be built on top of these services, such as automatic fusion of results, emotion model conversion, and service discovery. - * Interfacing with the user: parameter validation, error handling. - * Formatting: JSON-LD, Turtle/n-triples input and output, or simple text input - * Linked Data: senpy results are semantically annotated, using a series of well established vocabularies, and sane default URIs. - * User interface: a web UI where users can explore your service and test different settings - * A client to interact with the service. Currently only available in Python. - -Sharing your sentiment analysis with the world has never been easier! - -Check out the :doc:`plugins` if you have developed an analysis algorithm (e.g. sentiment analysis) and you want to publish it as a service. - -Architecture -============ - -The main component of a sentiment analysis service is the algorithm itself. However, for the algorithm to work, it needs to get the appropriate parameters from the user, format the results according to the defined API, interact with the user whn errors occur or more information is needed, etc. - -Senpy proposes a modular and dynamic architecture that allows: +These benefits are not limited to new services. +The community has developed wrappers for some proprietary and commercial services (such as sentiment140 and Meaning Cloud), so you can consult them as. +Senpy comes with a :ref:`built-in client`. -* Implementing different algorithms in a extensible way, yet offering a common interface. -* Offering common services that facilitate development, so developers can focus on implementing new and better algorithms. -The framework consists of two main modules: Senpy core, which is the building block of the service, and Senpy plugins, which consist of the analysis algorithm. The next figure depicts a simplified version of the processes involved in an analysis with the Senpy framework. +To achieve this goal, Senpy uses a Linked Data principled approach, based on the NIF (NLP Interchange Format) specification, and open vocabularies such as Marl and Onyx. +You can learn more about this in :doc:`vocabularies`. -.. image:: senpy-architecture.png - :width: 100% - :align: center +Check out :doc:`plugins` if you have developed an analysis algorithm (e.g. sentiment analysis) and you want to publish it as a service. diff --git a/docs/server.rst b/docs/server-cli.rst similarity index 62% rename from docs/server.rst rename to docs/server-cli.rst index 0255c0d..1bd4d5e 100644 --- a/docs/server.rst +++ b/docs/server-cli.rst @@ -5,10 +5,11 @@ The senpy server is launched via the `senpy` command: .. code:: text - usage: senpy [-h] [--level logging_level] [--debug] [--default-plugins] - [--host HOST] [--port PORT] [--plugins-folder PLUGINS_FOLDER] - [--only-install] [--only-list] [--data-folder DATA_FOLDER] - [--threaded] [--version] + usage: senpy [-h] [--level logging_level] [--log-format log_format] [--debug] + [--no-default-plugins] [--host HOST] [--port PORT] + [--plugins-folder PLUGINS_FOLDER] [--only-install] [--only-test] + [--test] [--only-list] [--data-folder DATA_FOLDER] + [--no-threaded] [--no-deps] [--version] [--allow-fail] Run a Senpy server @@ -16,20 +17,25 @@ The senpy server is launched via the `senpy` command: -h, --help show this help message and exit --level logging_level, -l logging_level Logging level + --log-format log_format + Logging format --debug, -d Run the application in debug mode - --default-plugins Load the default plugins + --no-default-plugins Do not load the default plugins --host HOST Use 0.0.0.0 to accept requests from any host. --port PORT, -p PORT Port to listen on. --plugins-folder PLUGINS_FOLDER, -f PLUGINS_FOLDER Where to look for plugins. --only-install, -i Do not run a server, only install plugin dependencies + --only-test Do not run a server, just test all plugins + --test, -t Test all plugins before launching the server --only-list, --list Do not run a server, only list plugins found --data-folder DATA_FOLDER, --data DATA_FOLDER Where to look for data. It be set with the SENPY_DATA environment variable as well. - --threaded Run a threaded server + --no-threaded Run the server without threading + --no-deps, -n Skip installing dependencies --version, -v Output the senpy version and exit - + --allow-fail, --fail Do not exit if some plugins fail to activate When launched, the server will recursively look for plugins in the specified plugins folder (the current working directory by default). @@ -40,9 +46,9 @@ Let's run senpy with the default plugins: .. code:: bash - senpy -f . --default-plugins + senpy -f . -Now go to `http://localhost:5000 `_, you should be greeted by the senpy playground: +Now open your browser and go to `http://localhost:5000 `_, where you should be greeted by the senpy playground: .. image:: senpy-playground.png :width: 100% @@ -51,9 +57,9 @@ Now go to `http://localhost:5000 `_, you should be greete The playground is a user-friendly way to test your plugins, but you can always use the service directly: `http://localhost:5000/api?input=hello `_. -By default, senpy will listen only on the `127.0.0.1` address. -That means you can only access the API from your (or localhost). -You can listen on a different address using the `--host` flag (e.g., 0.0.0.0). +By default, senpy will listen only on `127.0.0.1`. +That means you can only access the API from your PC (i.e. localhost). +You can listen on a different address using the `--host` flag (e.g., 0.0.0.0, to allow any computer to access it). The default port is 5000. You can change it with the `--port` flag. diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index d244600..0000000 --- a/docs/usage.rst +++ /dev/null @@ -1,15 +0,0 @@ -Usage ------ - -First of all, you need to install the package. -See :doc:`installation` for instructions. -Once installed, the `senpy` command should be available. - -.. toctree:: - :maxdepth: 1 - - server - SenpyClientUse - commandline - - diff --git a/example-plugins/README.md b/example-plugins/README.md index 4fef067..edf212a 100644 --- a/example-plugins/README.md +++ b/example-plugins/README.md @@ -1,6 +1,6 @@ This is a collection of plugins that exemplify certain aspects of plugin development with senpy. -The first series of plugins the `basic` ones. +The first series of plugins are the `basic` ones. Their starting point is a classification function defined in `basic.py`. They all include testing and running them as a script will run all tests. In ascending order of customization, the plugins are: @@ -19,5 +19,5 @@ In rest of the plugins show advanced topics: All of the plugins in this folder include a set of test cases and they are periodically tested with the latest version of senpy. -Additioanlly, for an example of stand-alone plugin that can be tested and deployed with docker, take a look at: lab.cluster.gsi.dit.upm.es/senpy/plugin-example +Additioanlly, for an example of stand-alone plugin that can be tested and deployed with docker, take a look at: lab.gsi.upm.es/senpy/plugin-example bbm diff --git a/example-plugins/basic.py b/example-plugins/basic.py index 39f23d6..1c0c78f 100644 --- a/example-plugins/basic.py +++ b/example-plugins/basic.py @@ -1,5 +1,5 @@ #!/usr/local/bin/python -# coding: utf-8 +# -*- coding: utf-8 -*- emoticons = { 'pos': [':)', ':]', '=)', ':D'], @@ -7,17 +7,19 @@ emoticons = { } emojis = { - 'pos': ['😁', '😂', '😃', '😄', '😆', '😅', '😄' '😍'], - 'neg': ['😢', '😡', '😠', '😞', '😖', '😔', '😓', '😒'] + 'pos': [u'😁', u'😂', u'😃', u'😄', u'😆', u'😅', u'😄', u'😍'], + 'neg': [u'😢', u'😡', u'😠', u'😞', u'😖', u'😔', u'😓', u'😒'] } def get_polarity(text, dictionaries=[emoticons, emojis]): polarity = 'marl:Neutral' + print('Input for get_polarity', text) for dictionary in dictionaries: for label, values in dictionary.items(): for emoticon in values: if emoticon and emoticon in text: polarity = label break + print('Polarity', polarity) return polarity diff --git a/example-plugins/basic_analyse_entry_plugin.py b/example-plugins/basic_analyse_entry_plugin.py index 8f5a4c3..b0b28bc 100644 --- a/example-plugins/basic_analyse_entry_plugin.py +++ b/example-plugins/basic_analyse_entry_plugin.py @@ -1,5 +1,5 @@ #!/usr/local/bin/python -# coding: utf-8 +# -*- coding: utf-8 -*- from senpy import easy_test, models, plugins @@ -18,13 +18,13 @@ class BasicAnalyseEntry(plugins.SentimentPlugin): 'default': 'marl:Neutral' } - def analyse_entry(self, entry, params): + def analyse_entry(self, entry, activity): polarity = basic.get_polarity(entry.text) polarity = self.mappings.get(polarity, self.mappings['default']) s = models.Sentiment(marl__hasPolarity=polarity) - s.prov(self) + s.prov(activity) entry.sentiments.append(s) yield entry diff --git a/example-plugins/basic_box_plugin.py b/example-plugins/basic_box_plugin.py index 3b18cf9..fa7d111 100644 --- a/example-plugins/basic_box_plugin.py +++ b/example-plugins/basic_box_plugin.py @@ -1,5 +1,5 @@ #!/usr/local/bin/python -# coding: utf-8 +# -*- coding: utf-8 -*- from senpy import easy_test, SentimentBox @@ -12,15 +12,13 @@ class BasicBox(SentimentBox): author = '@balkian' version = '0.1' - mappings = { - 'pos': 'marl:Positive', - 'neg': 'marl:Negative', - 'default': 'marl:Neutral' - } - - def predict_one(self, input): - output = basic.get_polarity(input) - return self.mappings.get(output, self.mappings['default']) + def predict_one(self, features, **kwargs): + output = basic.get_polarity(features[0]) + if output == 'pos': + return [1, 0, 0] + if output == 'neg': + return [0, 0, 1] + return [0, 1, 0] test_cases = [{ 'input': 'Hello :)', diff --git a/example-plugins/basic_plugin.py b/example-plugins/basic_plugin.py index 3c91e76..0f62f4e 100644 --- a/example-plugins/basic_plugin.py +++ b/example-plugins/basic_plugin.py @@ -1,37 +1,36 @@ #!/usr/local/bin/python -# coding: utf-8 +# -*- coding: utf-8 -*- -from senpy import easy_test, SentimentBox, MappingMixin +from senpy import easy_test, SentimentBox import basic -class Basic(MappingMixin, SentimentBox): +class Basic(SentimentBox): '''Provides sentiment annotation using a lexicon''' author = '@balkian' version = '0.1' - mappings = { - 'pos': 'marl:Positive', - 'neg': 'marl:Negative', - 'default': 'marl:Neutral' - } - - def predict_one(self, input): - return basic.get_polarity(input) + def predict_one(self, features, **kwargs): + output = basic.get_polarity(features[0]) + if output == 'pos': + return [1, 0, 0] + if output == 'neu': + return [0, 1, 0] + return [0, 0, 1] test_cases = [{ - 'input': 'Hello :)', + 'input': u'Hello :)', 'polarity': 'marl:Positive' }, { - 'input': 'So sad :(', + 'input': u'So sad :(', 'polarity': 'marl:Negative' }, { - 'input': 'Yay! Emojis 😁', + 'input': u'Yay! Emojis 😁', 'polarity': 'marl:Positive' }, { - 'input': 'But no emoticons 😢', + 'input': u'But no emoticons 😢', 'polarity': 'marl:Negative' }] diff --git a/example-plugins/configurable_plugin.py b/example-plugins/configurable_plugin.py index f857000..fcdd22b 100644 --- a/example-plugins/configurable_plugin.py +++ b/example-plugins/configurable_plugin.py @@ -1,5 +1,5 @@ #!/usr/local/bin/python -# coding: utf-8 +# -*- coding: utf-8 -*- from senpy import easy_test, models, plugins @@ -16,7 +16,7 @@ class Dictionary(plugins.SentimentPlugin): mappings = {'pos': 'marl:Positive', 'neg': 'marl:Negative'} - def analyse_entry(self, entry, params): + def analyse_entry(self, entry, *args, **kwargs): polarity = basic.get_polarity(entry.text, self.dictionaries) if polarity in self.mappings: polarity = self.mappings[polarity] diff --git a/senpy/plugins/example/emorand_plugin.py b/example-plugins/emorand_plugin.py similarity index 87% rename from senpy/plugins/example/emorand_plugin.py rename to example-plugins/emorand_plugin.py index c65d47d..42283cb 100644 --- a/senpy/plugins/example/emorand_plugin.py +++ b/example-plugins/emorand_plugin.py @@ -6,12 +6,13 @@ from senpy.models import EmotionSet, Emotion, Entry class EmoRand(EmotionPlugin): '''A sample plugin that returns a random emotion annotation''' + name = 'emotion-random' author = '@balkian' version = '0.1' url = "https://github.com/gsi-upm/senpy-plugins-community" onyx__usesEmotionModel = "emoml:big6" - def analyse_entry(self, entry, params): + def analyse_entry(self, entry, activity): category = "emoml:big6happiness" number = max(-1, min(1, random.gauss(0, 0.5))) if number > 0: @@ -19,7 +20,7 @@ class EmoRand(EmotionPlugin): emotionSet = EmotionSet() emotion = Emotion({"onyx:hasEmotionCategory": category}) emotionSet.onyx__hasEmotion.append(emotion) - emotionSet.prov__wasGeneratedBy = self.id + emotionSet.prov(activity) entry.emotions.append(emotionSet) yield entry @@ -27,6 +28,6 @@ class EmoRand(EmotionPlugin): params = dict() results = list() for i in range(100): - res = next(self.analyse_entry(Entry(nif__isString="Hello"), params)) + res = next(self.analyse_entry(Entry(nif__isString="Hello"), self.activity(params))) res.validate() results.append(res.emotions[0]['onyx:hasEmotion'][0]['onyx:hasEmotionCategory']) diff --git a/example-plugins/parameterized_plugin.py b/example-plugins/parameterized_plugin.py index 8a5798c..f915d38 100644 --- a/example-plugins/parameterized_plugin.py +++ b/example-plugins/parameterized_plugin.py @@ -1,5 +1,5 @@ #!/usr/local/bin/python -# coding: utf-8 +# -*- coding: utf-8 -*- from senpy import easy_test, models, plugins @@ -25,7 +25,8 @@ class ParameterizedDictionary(plugins.SentimentPlugin): } } - def analyse_entry(self, entry, params): + def analyse_entry(self, entry, activity): + params = activity.params positive_words = params['positive-words'].split(',') negative_words = params['negative-words'].split(',') dictionary = { @@ -35,7 +36,7 @@ class ParameterizedDictionary(plugins.SentimentPlugin): polarity = basic.get_polarity(entry.text, [dictionary]) s = models.Sentiment(marl__hasPolarity=polarity) - s.prov(self) + s.prov(activity) entry.sentiments.append(s) yield entry diff --git a/senpy/plugins/example/rand_plugin.py b/example-plugins/rand_plugin.py similarity index 82% rename from senpy/plugins/example/rand_plugin.py rename to example-plugins/rand_plugin.py index 45b3166..0c9363a 100644 --- a/senpy/plugins/example/rand_plugin.py +++ b/example-plugins/rand_plugin.py @@ -2,15 +2,16 @@ import random from senpy import SentimentPlugin, Sentiment, Entry -class Rand(SentimentPlugin): +class RandSent(SentimentPlugin): '''A sample plugin that returns a random sentiment annotation''' + name = 'sentiment-random' author = "@balkian" version = '0.1' url = "https://github.com/gsi-upm/senpy-plugins-community" marl__maxPolarityValue = '1' marl__minPolarityValue = "-1" - def analyse_entry(self, entry, params): + def analyse_entry(self, entry, activity): polarity_value = max(-1, min(1, random.gauss(0.2, 0.2))) polarity = "marl:Neutral" if polarity_value > 0: @@ -19,7 +20,7 @@ class Rand(SentimentPlugin): polarity = "marl:Negative" sentiment = Sentiment(marl__hasPolarity=polarity, marl__polarityValue=polarity_value) - sentiment.prov(self) + sentiment.prov(activity) entry.sentiments.append(sentiment) yield entry @@ -28,8 +29,9 @@ class Rand(SentimentPlugin): params = dict() results = list() for i in range(50): + activity = self.activity(params) res = next(self.analyse_entry(Entry(nif__isString="Hello"), - params)) + activity)) res.validate() results.append(res.sentiments[0]['marl:hasPolarity']) assert 'marl:Positive' in results diff --git a/example-plugins/sklearn/pipeline_plugin.py b/example-plugins/sklearn/pipeline_plugin.py index 29e8f36..a4d4b52 100644 --- a/example-plugins/sklearn/pipeline_plugin.py +++ b/example-plugins/sklearn/pipeline_plugin.py @@ -1,25 +1,20 @@ -from senpy import SentimentBox, MappingMixin, easy_test +from senpy import SentimentBox, easy_test from mypipeline import pipeline -class PipelineSentiment(MappingMixin, SentimentBox): - ''' - This is a pipeline plugin that wraps a classifier defined in another module - (mypipeline). - ''' +class PipelineSentiment(SentimentBox): + '''This is a pipeline plugin that wraps a classifier defined in another module +(mypipeline).''' author = '@balkian' version = 0.1 maxPolarityValue = 1 minPolarityValue = -1 - mappings = { - 1: 'marl:Positive', - -1: 'marl:Negative' - } - - def predict_one(self, input): - return pipeline.predict([input, ])[0] + def predict_one(self, features, **kwargs): + if pipeline.predict(features) > 0: + return [1, 0, 0] + return [0, 0, 1] test_cases = [ { diff --git a/extra-requirements.txt b/extra-requirements.txt index 4f16b72..1eac27a 100644 --- a/extra-requirements.txt +++ b/extra-requirements.txt @@ -1 +1 @@ -gsitk +gsitk>0.1.9.1 diff --git a/k8s/senpy-deployment.yaml b/k8s/senpy-deployment.yaml index 6d44402..4556734 100644 --- a/k8s/senpy-deployment.yaml +++ b/k8s/senpy-deployment.yaml @@ -15,8 +15,6 @@ spec: - name: senpy-latest image: $IMAGEWTAG imagePullPolicy: Always - args: - - "--default-plugins" resources: limits: memory: "512Mi" diff --git a/k8s/senpy-ingress.yaml b/k8s/senpy-ingress.yaml index 908af47..c46c129 100644 --- a/k8s/senpy-ingress.yaml +++ b/k8s/senpy-ingress.yaml @@ -12,3 +12,10 @@ spec: backend: serviceName: senpy-latest servicePort: 5000 + - host: latest.senpy.gsi.upm.es + http: + paths: + - path: / + backend: + serviceName: senpy-latest + servicePort: 5000 diff --git a/requirements.txt b/requirements.txt index c852f22..72164ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,6 @@ rdflib rdflib-jsonld numpy scipy -scikit-learn +scikit-learn>=0.20 responses +jmespath diff --git a/senpy/__main__.py b/senpy/__main__.py index 96b5c90..640b5ad 100644 --- a/senpy/__main__.py +++ b/senpy/__main__.py @@ -40,8 +40,14 @@ def main(): '-l', metavar='logging_level', type=str, - default="WARN", + default="INFO", help='Logging level') + parser.add_argument( + '--log-format', + metavar='log_format', + type=str, + default='%(asctime)s %(levelname)-10s %(name)-30s \t %(message)s', + help='Logging format') parser.add_argument( '--debug', '-d', @@ -49,10 +55,10 @@ def main(): default=False, help='Run the application in debug mode') parser.add_argument( - '--default-plugins', + '--no-default-plugins', action='store_true', default=False, - help='Load the default plugins') + help='Do not load the default plugins') parser.add_argument( '--host', type=str, @@ -68,7 +74,7 @@ def main(): '--plugins-folder', '-f', type=str, - default='.', + action='append', help='Where to look for plugins.') parser.add_argument( '--only-install', @@ -100,10 +106,10 @@ def main(): default=None, help='Where to look for data. It be set with the SENPY_DATA environment variable as well.') parser.add_argument( - '--threaded', - action='store_false', - default=True, - help='Run a threaded server') + '--no-threaded', + action='store_true', + default=False, + help='Run a single-threaded server') parser.add_argument( '--no-deps', '-n', @@ -123,30 +129,42 @@ def main(): default=False, help='Do not exit if some plugins fail to activate') args = parser.parse_args() + print('Senpy version {}'.format(senpy.__version__)) + print(sys.version) if args.version: - print('Senpy version {}'.format(senpy.__version__)) - print(sys.version) exit(1) rl = logging.getLogger() rl.setLevel(getattr(logging, args.level)) + logger_handler = rl.handlers[0] + + # First, generic formatter: + logger_handler.setFormatter(logging.Formatter(args.log_format)) + app = Flask(__name__) app.debug = args.debug - sp = Senpy(app, args.plugins_folder, - default_plugins=args.default_plugins, + sp = Senpy(app, + plugin_folder=None, + default_plugins=not args.no_default_plugins, data_folder=args.data_folder) + folders = list(args.plugins_folder) if args.plugins_folder else [] + if not folders: + folders.append(".") + for p in folders: + sp.add_folder(p) + + plugins = sp.plugins(plugin_type=None, is_activated=False) + maxname = max(len(x.name) for x in plugins) + maxversion = max(len(str(x.version)) for x in plugins) + print('Found {} plugins:'.format(len(plugins))) + for plugin in plugins: + import inspect + fpath = inspect.getfile(plugin.__class__) + print('\t{: <{maxname}} @ {: <{maxversion}} -> {}'.format(plugin.name, + plugin.version, + fpath, + maxname=maxname, + maxversion=maxversion)) if args.only_list: - plugins = sp.plugins() - maxname = max(len(x.name) for x in plugins) - maxversion = max(len(x.version) for x in plugins) - print('Found {} plugins:'.format(len(plugins))) - for plugin in plugins: - import inspect - fpath = inspect.getfile(plugin.__class__) - print('\t{: <{maxname}} @ {: <{maxversion}} -> {}'.format(plugin.name, - plugin.version, - fpath, - maxname=maxname, - maxversion=maxversion)) return if not args.no_deps: sp.install_deps() @@ -160,10 +178,13 @@ def main(): print('Senpy version {}'.format(senpy.__version__)) print('Server running on port %s:%d. Ctrl+C to quit' % (args.host, args.port)) - app.run(args.host, - args.port, - threaded=args.threaded, - debug=app.debug) + try: + app.run(args.host, + args.port, + threaded=not args.no_threaded, + debug=app.debug) + except KeyboardInterrupt: + print('Bye!') sp.deactivate_all() diff --git a/senpy/api.py b/senpy/api.py index ecc4d09..8e0503e 100644 --- a/senpy/api.py +++ b/senpy/api.py @@ -5,24 +5,31 @@ logger = logging.getLogger(__name__) boolean = [True, False] +processors = { + 'string_to_tuple': lambda p: p if isinstance(p, (tuple, list)) else tuple(p.split(',')) +} + API_PARAMS = { "algorithm": { "aliases": ["algorithms", "a", "algo"], "required": True, "default": 'default', + "processor": 'string_to_tuple', "description": ("Algorithms that will be used to process the request." "It may be a list of comma-separated names."), }, "expanded-jsonld": { "@id": "expanded-jsonld", - "aliases": ["expanded"], + "description": "use JSON-LD expansion to get full URIs", + "aliases": ["expanded", "expanded_jsonld"], "options": boolean, "required": True, "default": False }, - "with_parameters": { + "with-parameters": { "aliases": ['withparameters', - 'with-parameters'], + 'with_parameters'], + "description": "include initial parameters in the response", "options": boolean, "default": False, "required": True @@ -31,9 +38,67 @@ API_PARAMS = { "@id": "outformat", "aliases": ["o"], "default": "json-ld", + "description": """The data can be semantically formatted (JSON-LD, turtle or n-triples), +given as a list of comma-separated fields (see the fields option) or constructed from a Jinja2 +template (see the template option).""", "required": True, "options": ["json-ld", "turtle", "ntriples"], }, + "template": { + "@id": "template", + "required": False, + "description": """Jinja2 template for the result. The input data for the template will +be the results as a dictionary. +For example: + +Consider the results before templating: + +``` +[{ + "@type": "entry", + "onyx:hasEmotionSet": [], + "nif:isString": "testing the template", + "marl:hasOpinion": [ + { + "@type": "sentiment", + "marl:hasPolarity": "marl:Positive" + } + ] +}] +``` + + +And the template: + +``` +{% for entry in entries %} +{{ entry["nif:isString"] | upper }},{{entry.sentiments[0]["marl:hasPolarity"].split(":")[1]}} +{% endfor %} +``` + +The final result would be: + +``` +TESTING THE TEMPLATE,Positive +``` +""" + + }, + "fields": { + "@id": "fields", + "required": False, + "description": """A jmespath selector, that can be used to extract a new dictionary, array or value +from the results. +jmespath is a powerful query language for json and/or dictionaries. +It allows you to change the structure (and data) of your objects through queries. + +e.g., the following expression gets a list of `[emotion label, intensity]` for each entry: +`entries[]."onyx:hasEmotionSet"[]."onyx:hasEmotion"[]["onyx:hasEmotionCategory","onyx:hasEmotionIntensity"]` + +For more information, see: https://jmespath.org + +""" + }, "help": { "@id": "help", "description": "Show additional help to know more about the possible parameters", @@ -44,21 +109,39 @@ API_PARAMS = { }, "verbose": { "@id": "verbose", - "description": ("Show all help, including the common API parameters, or " - "only plugin-related info"), + "description": "Show all properties in the result", "aliases": ["v"], "required": True, "options": boolean, - "default": True + "default": False + }, + "aliases": { + "@id": "aliases", + "description": "Replace JSON properties with their aliases", + "aliases": [], + "required": True, + "options": boolean, + "default": False }, - "emotionModel": { + "emotion-model": { "@id": "emotionModel", - "aliases": ["emoModel"], + "description": """Emotion model to use in the response. +Senpy will try to convert the output to this model automatically. + +Examples: `wna:liking` and `emoml:big6`. + """, + "aliases": ["emoModel", "emotionModel"], "required": False }, "conversion": { "@id": "conversion", - "description": "How to show the elements that have (not) been converted", + "description": """How to show the elements that have (not) been converted. + +* full: converted and original elements will appear side-by-side +* filtered: only converted elements will be shown +* nested: converted elements will be shown, and they will include a link to the original element +(using `prov:wasGeneratedBy`). +""", "required": True, "options": ["filtered", "nested", "full"], "default": "full" @@ -68,9 +151,10 @@ API_PARAMS = { EVAL_PARAMS = { "algorithm": { "aliases": ["plug", "p", "plugins", "algorithms", 'algo', 'a', 'plugin'], - "description": "Plugins to be evaluated", + "description": "Plugins to evaluate", "required": True, - "help": "See activated plugins in /plugins" + "help": "See activated plugins in /plugins", + "processor": API_PARAMS['algorithm']['processor'] }, "dataset": { "aliases": ["datasets", "data", "d"], @@ -81,18 +165,19 @@ EVAL_PARAMS = { } PLUGINS_PARAMS = { - "plugin_type": { + "plugin-type": { "@id": "pluginType", "description": 'What kind of plugins to list', - "aliases": ["pluginType"], + "aliases": ["pluginType", "plugin_type"], "required": True, "default": 'analysisPlugin' } } WEB_PARAMS = { - "inHeaders": { - "aliases": ["headers"], + "in-headers": { + "aliases": ["headers", "inheaders", "inHeaders", "in-headers", "in_headers"], + "description": "Only include the JSON-LD context in the headers", "required": True, "default": False, "options": boolean @@ -100,8 +185,8 @@ WEB_PARAMS = { } CLI_PARAMS = { - "plugin_folder": { - "aliases": ["folder"], + "plugin-folder": { + "aliases": ["folder", "plugin_folder"], "required": True, "default": "." }, @@ -116,6 +201,7 @@ NIF_PARAMS = { }, "intype": { "@id": "intype", + "description": "input type", "aliases": ["t"], "required": False, "default": "direct", @@ -123,6 +209,7 @@ NIF_PARAMS = { }, "informat": { "@id": "informat", + "description": "input format", "aliases": ["f"], "required": False, "default": "text", @@ -130,17 +217,20 @@ NIF_PARAMS = { }, "language": { "@id": "language", + "description": "language of the input", "aliases": ["l"], "required": False, }, "prefix": { "@id": "prefix", + "description": "prefix to use for new entities", "aliases": ["p"], "required": True, "default": "", }, "urischeme": { "@id": "urischeme", + "description": "scheme for NIF URIs", "aliases": ["u"], "required": False, "default": "RFC5147String", @@ -171,16 +261,19 @@ def parse_params(indict, *specs): if alias in indict and alias != param: outdict[param] = indict[alias] del outdict[alias] - continue + break if param not in outdict: if "default" in options: # We assume the default is correct outdict[param] = options["default"] elif options.get("required", False): wrong_params[param] = spec[param] - elif "options" in options: + continue + if 'processor' in options: + outdict[param] = processors[options['processor']](outdict[param]) + if "options" in options: if options["options"] == boolean: - outdict[param] = str(outdict[param]).lower() in ['true', '1'] + outdict[param] = str(outdict[param]).lower() in ['true', '1', ''] elif outdict[param] not in options["options"]: wrong_params[param] = spec[param] if wrong_params: @@ -253,7 +346,7 @@ def get_extra_params(plugins): return params -def parse_analysis(params, plugins): +def parse_analyses(params, plugins): ''' Parse the given parameters individually for each plugin, and get a list of the parameters that belong to each of the plugins. Each item can then be used in the plugin.analyse_entries method. @@ -305,7 +398,7 @@ def parse_call(params): params = parse_params(params, NIF_PARAMS) if params['informat'] == 'text': results = Results() - entry = Entry(nif__isString=params['input'], id='#') # Use @base + entry = Entry(nif__isString=params['input'], id='prefix:') # Use @base results.entries.append(entry) elif params['informat'] == 'json-ld': results = from_string(params['input'], cls=Results) diff --git a/senpy/blueprints.py b/senpy/blueprints.py index d6ddd5d..a3b4f0a 100644 --- a/senpy/blueprints.py +++ b/senpy/blueprints.py @@ -24,6 +24,8 @@ from . import api from .version import __version__ from functools import wraps +from .gsitk_compat import GSITK_AVAILABLE + import logging import json import base64 @@ -63,44 +65,44 @@ def get_params(req): return indict -def encoded_url(url=None, base=None): +def encode_url(url=None): code = '' if not url: - if request.method == 'GET': - url = request.full_path[1:] # Remove the first slash - else: - hash(frozenset(tuple(request.parameters.items()))) - code = 'hash:{}'.format(hash) + url = request.parameters.get('prefix', request.full_path[1:] + '#') + return code or base64.urlsafe_b64encode(url.encode()).decode() - code = code or base64.urlsafe_b64encode(url.encode()).decode() - if base: - return base + code - return url_for('api.decode', code=code, _external=True) +def url_for_code(code, base=None): + # if base: + # return base + code + # return url_for('api.decode', code=code, _external=True) + # This was producing unique yet very long URIs, which wasn't ideal for visualization. + return 'http://senpy.invalid/' def decoded_url(code, base=None): - if code.startswith('hash:'): - raise Exception('Can not decode a URL for a POST request') - base = base or request.url_root path = base64.urlsafe_b64decode(code.encode()).decode() + if path[:4] == 'http': + return path + base = base or request.url_root return base + path @demo_blueprint.route('/') def index(): - ev = str(get_params(request).get('evaluation', False)) - evaluation_enabled = ev.lower() not in ['false', 'no', 'none'] + # ev = str(get_params(request).get('evaluation', True)) + # evaluation_enabled = ev.lower() not in ['false', 'no', 'none'] + evaluation_enabled = GSITK_AVAILABLE return render_template("index.html", evaluation=evaluation_enabled, version=__version__) -@api_blueprint.route('/contexts/.jsonld') -def context(entity="context"): +@api_blueprint.route('/contexts/') +def context(code=''): context = Response._context - context['@vocab'] = url_for('ns.index', _external=True) + context['@base'] = url_for('api.decode', code=code, _external=True) context['endpoint'] = url_for('api.api_root', _external=True) return jsonify({"@context": context}) @@ -130,26 +132,59 @@ def schema(schema="definitions"): def basic_api(f): default_params = { - 'inHeaders': False, + 'in-headers': False, 'expanded-jsonld': False, 'outformat': None, - 'with_parameters': True, + 'with-parameters': True, } @wraps(f) def decorated_function(*args, **kwargs): raw_params = get_params(request) - logger.info('Getting request: {}'.format(raw_params)) + # logger.info('Getting request: {}'.format(raw_params)) + logger.debug('Getting request. Params: {}'.format(raw_params)) headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)} params = default_params + mime = request.accept_mimetypes\ + .best_match(MIMETYPES.keys(), + DEFAULT_MIMETYPE) + + mimeformat = MIMETYPES.get(mime, DEFAULT_FORMAT) + outformat = mimeformat + try: params = api.parse_params(raw_params, api.WEB_PARAMS, api.API_PARAMS) + outformat = params.get('outformat', mimeformat) if hasattr(request, 'parameters'): request.parameters.update(params) else: request.parameters = params response = f(*args, **kwargs) + + if 'parameters' in response and not params['with-parameters']: + del response.parameters + + logger.debug('Response: {}'.format(response)) + + prefix = params.get('prefix') + code = encode_url(prefix) + + return response.flask( + in_headers=params['in-headers'], + headers=headers, + prefix=prefix or url_for_code(code), + base=prefix, + context_uri=url_for('api.context', + code=code, + _external=True), + outformat=outformat, + expanded=params['expanded-jsonld'], + template=params.get('template'), + verbose=params['verbose'], + aliases=params['aliases'], + fields=params.get('fields')) + except (Exception) as ex: if current_app.debug or current_app.config['TESTING']: raise @@ -159,56 +194,48 @@ def basic_api(f): response = ex response.parameters = raw_params logger.exception(ex) - - if 'parameters' in response and not params['with_parameters']: - del response.parameters - - logger.info('Response: {}'.format(response)) - mime = request.accept_mimetypes\ - .best_match(MIMETYPES.keys(), - DEFAULT_MIMETYPE) - - mimeformat = MIMETYPES.get(mime, DEFAULT_FORMAT) - outformat = params['outformat'] or mimeformat - - return response.flask( - in_headers=params['inHeaders'], - headers=headers, - prefix=params.get('prefix', encoded_url()), - context_uri=url_for('api.context', - entity=type(response).__name__, - _external=True), - outformat=outformat, - expanded=params['expanded-jsonld']) + return response.flask( + outformat=outformat, + expanded=params['expanded-jsonld'], + verbose=params.get('verbose', True), + ) return decorated_function -@api_blueprint.route('/', defaults={'plugin': None}, methods=['POST', 'GET']) -@api_blueprint.route('/', methods=['POST', 'GET']) +@api_blueprint.route('/', defaults={'plugins': None}, methods=['POST', 'GET']) +@api_blueprint.route('/', methods=['POST', 'GET']) @basic_api -def api_root(plugin): - if plugin: +def api_root(plugins): + if plugins: if request.parameters['algorithm'] != api.API_PARAMS['algorithm']['default']: raise Error('You cannot specify the algorithm with a parameter and a URL variable.' ' Please, remove one of them') - request.parameters['algorithm'] = tuple(plugin.replace('+', '/').split('/')) + plugins = plugins.replace('+', ',').replace('/', ',') + plugins = api.processors['string_to_tuple'](plugins) + else: + plugins = request.parameters['algorithm'] - plugin = request.parameters['algorithm'] + print(plugins) sp = current_app.senpy - plugins = sp.get_plugins(plugin) + plugins = sp.get_plugins(plugins) if request.parameters['help']: - apis = [] - if request.parameters['verbose']: - apis.append(api.BUILTIN_PARAMS) + apis = [api.WEB_PARAMS, api.API_PARAMS, api.NIF_PARAMS] + # Verbose is set to False as default, but we want it to default to + # True for help. This checks the original value, to make sure it wasn't + # set by default. + if not request.parameters['verbose'] and get_params(request).get('verbose'): + apis = [] + if request.parameters['algorithm'] == ['default', ]: + plugins = [] allparameters = api.get_all_params(plugins, *apis) response = Help(valid_parameters=allparameters) return response req = api.parse_call(request.parameters) - analysis = api.parse_analysis(req.parameters, plugins) - results = current_app.senpy.analyse(req, analysis) + analyses = api.parse_analyses(req.parameters, plugins) + results = current_app.senpy.analyse(req, analyses) return results @@ -230,8 +257,8 @@ def evaluate(): def plugins(): sp = current_app.senpy params = api.parse_params(request.parameters, api.PLUGINS_PARAMS) - ptype = params.get('plugin_type') - plugins = list(sp.plugins(plugin_type=ptype)) + ptype = params.get('plugin-type') + plugins = list(sp.analysis_plugins(plugin_type=ptype)) dic = Plugins(plugins=plugins) return dic diff --git a/senpy/cli.py b/senpy/cli.py index af1eed9..e0ee796 100644 --- a/senpy/cli.py +++ b/senpy/cli.py @@ -1,3 +1,5 @@ +from __future__ import print_function + import sys from .models import Error from .extensions import Senpy @@ -27,8 +29,8 @@ def main_function(argv): api.CLI_PARAMS, api.API_PARAMS, api.NIF_PARAMS) - plugin_folder = params['plugin_folder'] - default_plugins = params.get('default-plugins', False) + plugin_folder = params['plugin-folder'] + default_plugins = not params.get('no-default-plugins', False) sp = Senpy(default_plugins=default_plugins, plugin_folder=plugin_folder) request = api.parse_call(params) algos = sp.get_plugins(request.parameters.get('algorithm', None)) @@ -48,7 +50,7 @@ def main(): res = main_function(sys.argv[1:]) print(res.serialize()) except Error as err: - print(err.serialize()) + print(err.serialize(), file=sys.stderr) sys.exit(2) diff --git a/senpy/extensions.py b/senpy/extensions.py index 1386126..2f5f6a6 100644 --- a/senpy/extensions.py +++ b/senpy/extensions.py @@ -7,6 +7,7 @@ standard_library.install_aliases() from . import plugins, api from .models import Error, AggregatedEvaluation +from .plugins import AnalysisPlugin from .blueprints import api_blueprint, demo_blueprint, ns_blueprint from threading import Thread @@ -54,6 +55,7 @@ class Senpy(object): self.app = app if app is not None: self.init_app(app) + self._conversion_candidates = {} def init_app(self, app): """ Initialise a flask app to add plugins to its context """ @@ -74,14 +76,18 @@ class Senpy(object): def add_plugin(self, plugin): self._plugins[plugin.name.lower()] = plugin + self._conversion_candidates = {} def delete_plugin(self, plugin): del self._plugins[plugin.name.lower()] def plugins(self, plugin_type=None, is_activated=True, **kwargs): """ Return the plugins registered for a given application. Filtered by criteria """ - return list(plugins.pfilter(self._plugins, plugin_type=plugin_type, - is_activated=is_activated, **kwargs)) + return sorted(plugins.pfilter(self._plugins, + plugin_type=plugin_type, + is_activated=is_activated, + **kwargs), + key=lambda x: x.id) def get_plugin(self, name, default=None): if name == 'default': @@ -115,10 +121,10 @@ class Senpy(object): pass # Assume it is a tuple or a list return tuple(self.get_plugin(n) for n in name) - @property - def analysis_plugins(self): + def analysis_plugins(self, **kwargs): """ Return only the analysis plugins that are active""" - return self.plugins(plugin_type='analysisPlugin', is_activated=True) + candidates = self.plugins(**kwargs) + return list(plugins.pfilter(candidates, plugin_type=AnalysisPlugin)) def add_folder(self, folder, from_root=False): """ Find plugins in this folder and add them to this instance """ @@ -144,14 +150,17 @@ class Senpy(object): analysis = pending[0] results = analysis.run(req) - results.analysis.append(analysis) + results.activities.append(analysis) done += analysis return self._process(results, pending[1:], done) def install_deps(self): - plugins.install_deps(*self.plugins()) + logger.info('Installing dependencies') + # If a plugin is activated, its dependencies should already be installed + # Otherwise, it would've failed to activate. + plugins.install_deps(*self.plugins(is_activated=False)) - def analyse(self, request, analysis=None): + def analyse(self, request, analyses=None): """ Main method that analyses a request, either from CLI or HTTP. It takes a processed request, provided by the user, as returned @@ -162,17 +171,17 @@ class Senpy(object): status=404, message=("No plugins found." " Please install one.")) - if analysis is None: + if analyses is None: plugins = self.get_plugins(request.parameters['algorithm']) - analysis = api.parse_analysis(request.parameters, plugins) + analyses = api.parse_analyses(request.parameters, plugins) logger.debug("analysing request: {}".format(request)) - results = self._process(request, analysis) + results = self._process(request, analyses) logger.debug("Got analysis result: {}".format(results)) - results = self.postprocess(results) + results = self.postprocess(results, analyses) logger.debug("Returning post-processed result: {}".format(results)) return results - def convert_emotions(self, resp): + def convert_emotions(self, resp, analyses): """ Conversion of all emotions in a response **in place**. In addition to converting from one model to another, it has @@ -180,45 +189,50 @@ class Senpy(object): Needless to say, this is far from an elegant solution, but it works. @todo refactor and clean up """ - plugins = resp.analysis + logger.debug("Converting emotions") if 'parameters' not in resp: + logger.debug("NO PARAMETERS") return resp params = resp['parameters'] - toModel = params.get('emotionModel', None) + toModel = params.get('emotion-model', None) if not toModel: + logger.debug("NO tomodel PARAMETER") return resp logger.debug('Asked for model: {}'.format(toModel)) output = params.get('conversion', None) - candidates = {} - for plugin in plugins: - try: - fromModel = plugin.get('onyx:usesEmotionModel', None) - candidates[plugin.id] = next(self._conversion_candidates(fromModel, toModel)) - logger.debug('Analysis plugin {} uses model: {}'.format( - plugin.id, fromModel)) - except StopIteration: - e = Error(('No conversion plugin found for: ' - '{} -> {}'.format(fromModel, toModel)), - status=404) - e.original_response = resp - e.parameters = params - raise e + newentries = [] done = [] for i in resp.entries: + if output == "full": newemotions = copy.deepcopy(i.emotions) else: newemotions = [] for j in i.emotions: - plugname = j['prov:wasGeneratedBy'] - candidate = candidates[plugname] - done.append({'plugin': candidate, 'parameters': params}) + activity = j['prov:wasGeneratedBy'] + act = resp.activity(activity) + if not act: + raise Error('Could not find the emotion model for {}'.format(activity)) + fromModel = act.plugin['onyx:usesEmotionModel'] + if toModel == fromModel: + continue + candidate = self._conversion_candidate(fromModel, toModel) + if not candidate: + e = Error(('No conversion plugin found for: ' + '{} -> {}'.format(fromModel, toModel)), + status=404) + e.original_response = resp + e.parameters = params + raise e + + analysis = candidate.activity(params) + done.append(analysis) for k in candidate.convert(j, fromModel, toModel, params): - k.prov__wasGeneratedBy = candidate.id + k.prov__wasGeneratedBy = analysis.id if output == 'nested': k.prov__wasDerivedFrom = j newemotions.append(k) @@ -227,26 +241,36 @@ class Senpy(object): resp.entries = newentries return resp - def _conversion_candidates(self, fromModel, toModel): - candidates = self.plugins(plugin_type=plugins.EmotionConversion) - for candidate in candidates: - for pair in candidate.onyx__doesConversion: - logging.debug(pair) - if candidate.can_convert(fromModel, toModel): - yield candidate + def _conversion_candidate(self, fromModel, toModel): + if not self._conversion_candidates: + candidates = {} + for conv in self.plugins(plugin_type=plugins.EmotionConversion): + for pair in conv.onyx__doesConversion: + logging.debug(pair) + key = (pair['onyx:conversionFrom'], pair['onyx:conversionTo']) + if key not in candidates: + candidates[key] = [] + candidates[key].append(conv) + self._conversion_candidates = candidates + + key = (fromModel, toModel) + if key not in self._conversion_candidates: + return None + return self._conversion_candidates[key][0] - def postprocess(self, response): + def postprocess(self, response, analyses): ''' Transform the results from the analysis plugins. It has some pre-defined post-processing like emotion conversion, and it also allows plugins to auto-select themselves. ''' - response = self.convert_emotions(response) + response = self.convert_emotions(response, analyses) for plug in self.plugins(plugin_type=plugins.PostProcessing): - if plug.check(response, response.analysis): - response = plug.process(response) + if plug.check(response, response.activities): + activity = plug.activity(response.parameters) + response = plug.process(response, activity) return response def _get_datasets(self, request): @@ -286,13 +310,16 @@ class Senpy(object): results = AggregatedEvaluation() results.parameters = params datasets = self._get_datasets(results) - plugins = [] - for plugname in params.algorithm: - plugins = self.get_plugin(plugname) - - for eval in plugins.evaluate(plugins, datasets): + plugs = [] + for plugname in params['algorithm']: + plugs = self.get_plugins(plugname) + for plug in plugs: + if not isinstance(plug, plugins.Evaluable): + raise Exception('Plugin {} can not be evaluated', plug.id) + + for eval in plugins.evaluate(plugs, datasets): results.evaluations.append(eval) - if 'with_parameters' not in results.parameters: + if 'with-parameters' not in results.parameters: del results.parameters logger.debug("Returning evaluation result: {}".format(results)) return results @@ -300,8 +327,7 @@ class Senpy(object): @property def default_plugin(self): if not self._default or not self._default.is_activated: - candidates = self.plugins( - plugin_type='analysisPlugin', is_activated=True) + candidates = self.analysis_plugins() if len(candidates) > 0: self._default = candidates[0] else: @@ -336,22 +362,15 @@ class Senpy(object): ps.append(self.deactivate_plugin(plug, sync=sync)) return ps - def _set_active(self, plugin, active=True, *args, **kwargs): - ''' We're using a variable in the plugin itself to activate/deactivate plugins.\ - Note that plugins may activate themselves by setting this variable. - ''' - plugin.is_activated = active - def _activate(self, plugin): success = False with plugin._lock: if plugin.is_activated: return - plugin.activate() + plugin._activate() msg = "Plugin activated: {}".format(plugin.name) logger.info(msg) - success = True - self._set_active(plugin, success) + success = plugin.is_activated return success def activate_plugin(self, plugin_name, sync=True): @@ -375,7 +394,7 @@ class Senpy(object): with plugin._lock: if not plugin.is_activated: return - plugin.deactivate() + plugin._deactivate() logger.info("Plugin deactivated: {}".format(plugin.name)) def deactivate_plugin(self, plugin_name, sync=True): @@ -385,13 +404,11 @@ class Senpy(object): message="Plugin not found: {}".format(plugin_name), status=404) plugin = self._plugins[plugin_name] - self._set_active(plugin, False) - if sync or not getattr(plugin, 'async', True) or not getattr( plugin, 'sync', False): - self._deactivate(plugin) + plugin._deactivate() else: - th = Thread(target=partial(self._deactivate, plugin)) + th = Thread(target=plugin.deactivate) th.start() return th diff --git a/senpy/gsitk_compat.py b/senpy/gsitk_compat.py index 3cc143f..c91c1d7 100644 --- a/senpy/gsitk_compat.py +++ b/senpy/gsitk_compat.py @@ -16,16 +16,16 @@ def raise_exception(*args, **kwargs): try: gsitk_distro = get_distribution("gsitk") GSITK_VERSION = parse_version(gsitk_distro.version) - GSITK_AVAILABLE = GSITK_VERSION > parse_version("0.1.9.1") # Earlier versions have a bug -except DistributionNotFound: - GSITK_AVAILABLE = False - GSITK_VERSION = () -if GSITK_AVAILABLE: from gsitk.datasets.datasets import DatasetManager - from gsitk.evaluation.evaluation import Evaluation as Eval + from gsitk.evaluation.evaluation import Evaluation as Eval # noqa: F401 + from gsitk.evaluation.evaluation import EvalPipeline # noqa: F401 from sklearn.pipeline import Pipeline modules = locals() -else: + GSITK_AVAILABLE = True +except (DistributionNotFound, ImportError) as err: + logger.debug('Error importing GSITK: {}'.format(err)) logger.warning(IMPORTMSG) + GSITK_AVAILABLE = False + GSITK_VERSION = () DatasetManager = Eval = Pipeline = raise_exception diff --git a/senpy/meta.py b/senpy/meta.py index 445513e..a8a5b6b 100644 --- a/senpy/meta.py +++ b/senpy/meta.py @@ -34,6 +34,7 @@ class BaseMeta(ABCMeta): def __new__(mcs, name, bases, attrs, **kwargs): register_afterwards = False defaults = {} + aliases = {} attrs = mcs.expand_with_schema(name, attrs) if 'schema' in attrs: @@ -41,17 +42,21 @@ class BaseMeta(ABCMeta): for base in bases: if hasattr(base, '_defaults'): defaults.update(getattr(base, '_defaults')) + if hasattr(base, '_aliases'): + aliases.update(getattr(base, '_aliases')) info, rest = mcs.split_attrs(attrs) for i in list(info.keys()): if isinstance(info[i], _Alias): - fget, fset, fdel = make_property(info[i].indict) - rest[i] = property(fget=fget, fset=fset, fdel=fdel) + aliases[i] = info[i].indict + if info[i].default is not None: + defaults[i] = info[i].default else: defaults[i] = info[i] rest['_defaults'] = defaults + rest['_aliases'] = aliases cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), rest) @@ -86,7 +91,7 @@ class BaseMeta(ABCMeta): resolver = jsonschema.RefResolver(schema_path, schema) if '@type' not in attrs: - attrs['@type'] = "".join((name[0].lower(), name[1:])) + attrs['@type'] = name attrs['_schema_file'] = schema_file attrs['schema'] = schema attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver) @@ -140,9 +145,11 @@ class BaseMeta(ABCMeta): return temp -def make_property(key): +def make_property(key, default=None): def fget(self): + if default: + return self.get(key, copy.copy(default)) return self[key] def fdel(self): @@ -168,7 +175,7 @@ class CustomDict(MutableMapping, object): ''' _defaults = {} - _map_attr_key = {'id': '@id'} + _aliases = {'id': '@id'} def __init__(self, *args, **kwargs): super(CustomDict, self).__init__() @@ -177,13 +184,13 @@ class CustomDict(MutableMapping, object): for arg in args: self.update(arg) for k, v in kwargs.items(): - self[self._attr_to_key(k)] = v + self[k] = v return self - def serializable(self): + def serializable(self, **kwargs): def ser_or_down(item): if hasattr(item, 'serializable'): - return item.serializable() + return item.serializable(**kwargs) elif isinstance(item, dict): temp = dict() for kp in item: @@ -195,10 +202,9 @@ class CustomDict(MutableMapping, object): else: return item - return ser_or_down(self.as_dict()) + return ser_or_down(self.as_dict(**kwargs)) def __getitem__(self, key): - key = self._key_to_attr(key) return self.__dict__[key] def __setitem__(self, key, value): @@ -206,9 +212,23 @@ class CustomDict(MutableMapping, object): key = self._key_to_attr(key) return setattr(self, key, value) - def as_dict(self): - return {self._attr_to_key(k): v for k, v in self.__dict__.items() - if not self._internal_key(k)} + def __delitem__(self, key): + key = self._key_to_attr(key) + del self.__dict__[key] + + def as_dict(self, verbose=True, aliases=False): + attrs = self.__dict__.keys() + if not verbose and hasattr(self, '_terse_keys'): + attrs = self._terse_keys + ['@type', '@id'] + res = {k: getattr(self, k) for k in attrs + if not self._internal_key(k) and hasattr(self, k)} + if not aliases: + return res + for k, ok in self._aliases.items(): + if ok in res: + res[k] = getattr(res, ok) + del res[ok] + return res def __iter__(self): return (k for k in self.__dict__ if not self._internal_key(k)) @@ -216,29 +236,38 @@ class CustomDict(MutableMapping, object): def __len__(self): return len(self.__dict__) - def __delitem__(self, key): - del self.__dict__[key] - def update(self, other): for k, v in other.items(): self[k] = v def _attr_to_key(self, key): key = key.replace("__", ":", 1) - key = self._map_attr_key.get(key, key) + key = self._aliases.get(key, key) return key def _key_to_attr(self, key): if self._internal_key(key): return key - key = key.replace(":", "__", 1) + + if key in self._aliases: + key = self._aliases[key] + else: + key = key.replace(":", "__", 1) return key def __getattr__(self, key): - try: - return self.__dict__[self._attr_to_key(key)] - except KeyError: - raise AttributeError + nkey = self._attr_to_key(key) + if nkey in self.__dict__: + return self.__dict__[nkey] + elif nkey == key: + raise AttributeError("Key not found: {}".format(key)) + return getattr(self, nkey) + + def __setattr__(self, key, value): + super(CustomDict, self).__setattr__(self._attr_to_key(key), value) + + def __delattr__(self, key): + super(CustomDict, self).__delattr__(self._attr_to_key(key)) @staticmethod def _internal_key(key): @@ -251,8 +280,8 @@ class CustomDict(MutableMapping, object): return json.dumps(self.serializable(), sort_keys=True, indent=4) -_Alias = namedtuple('Alias', 'indict') +_Alias = namedtuple('Alias', ['indict', 'default']) -def alias(key): - return _Alias(key) +def alias(key, default=None): + return _Alias(key, default) diff --git a/senpy/models.py b/senpy/models.py index d20f076..447568e 100644 --- a/senpy/models.py +++ b/senpy/models.py @@ -12,6 +12,8 @@ standard_library.install_aliases() from future.utils import with_metaclass from past.builtins import basestring +from jinja2 import Environment, BaseLoader + import time import copy import json @@ -21,6 +23,7 @@ from flask import Response as FlaskResponse from pyld import jsonld import logging +import jmespath logging.getLogger('rdflib').setLevel(logging.WARN) logger = logging.getLogger(__name__) @@ -31,8 +34,9 @@ from rdflib import Graph from .meta import BaseMeta, CustomDict, alias DEFINITIONS_FILE = 'definitions.json' -CONTEXT_PATH = os.path.join( - os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld') +CONTEXT_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'schemas', + 'context.jsonld') def get_schema_path(schema_file, absolute=False): @@ -132,13 +136,10 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)): if auto_id: self.id - if '@type' not in self: - logger.warning('Created an instance of an unknown model') - @property def id(self): if '@id' not in self: - self['@id'] = '_:{}_{}'.format(type(self).__name__, time.time()) + self['@id'] = 'prefix:{}_{}'.format(type(self).__name__, time.time()) return self['@id'] @id.setter @@ -174,24 +175,33 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)): headers=headers, mimetype=mimetype) - def serialize(self, format='json-ld', with_mime=False, **kwargs): - js = self.jsonld(**kwargs) - content = json.dumps(js, indent=2, sort_keys=True) - if format == 'json-ld': + def serialize(self, format='json-ld', with_mime=False, + template=None, prefix=None, fields=None, **kwargs): + js = self.jsonld(prefix=prefix, **kwargs) + if template is not None: + rtemplate = Environment(loader=BaseLoader).from_string(template) + content = rtemplate.render(**self) + mimetype = 'text' + elif fields is not None: + # Emulate field selection by constructing a template + content = json.dumps(jmespath.search(fields, js)) + mimetype = 'text' + elif format == 'json-ld': + content = json.dumps(js, indent=2, sort_keys=True) mimetype = "application/json" elif format in ['turtle', 'ntriples']: + content = json.dumps(js, indent=2, sort_keys=True) logger.debug(js) - base = kwargs.get('prefix') + context = [self._context, {'prefix': prefix, '@base': prefix}] g = Graph().parse( data=content, format='json-ld', - base=base, - context=[self._context, - {'@base': base}]) + prefix=prefix, + context=context) logger.debug( 'Parsing with prefix: {}'.format(kwargs.get('prefix'))) content = g.serialize(format=format, - base=base).decode('utf-8') + prefix=prefix).decode('utf-8') mimetype = 'text/{}'.format(format) else: raise Error('Unknown outformat: {}'.format(format)) @@ -204,14 +214,25 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)): with_context=False, context_uri=None, prefix=None, - expanded=False): + base=None, + expanded=False, + **kwargs): - result = self.serializable() + result = self.serializable(**kwargs) if expanded: result = jsonld.expand( - result, options={'base': prefix, - 'expandContext': self._context})[0] + result, + options={ + 'expandContext': [ + self._context, + { + 'prefix': prefix, + 'endpoint': prefix + } + ] + } + )[0] if not with_context: try: del result['@context'] @@ -239,7 +260,7 @@ def subtypes(): return BaseMeta._subtypes -def from_dict(indict, cls=None): +def from_dict(indict, cls=None, warn=True): if not cls: target = indict.get('@type', None) cls = BaseModel @@ -247,6 +268,10 @@ def from_dict(indict, cls=None): cls = subtypes()[target] except KeyError: pass + + if cls == BaseModel and warn: + logger.warning('Created an instance of an unknown model') + outdict = dict() for k, v in indict.items(): if k == '@context': @@ -266,22 +291,24 @@ def from_string(string, **kwargs): return from_dict(json.loads(string), **kwargs) -def from_json(injson): +def from_json(injson, **kwargs): indict = json.loads(injson) - return from_dict(indict) + return from_dict(indict, **kwargs) class Entry(BaseModel): schema = 'entry' text = alias('nif:isString') + sentiments = alias('marl:hasOpinion', []) + emotions = alias('onyx:hasEmotionSet', []) class Sentiment(BaseModel): schema = 'sentiment' polarity = alias('marl:hasPolarity') - polarityValue = alias('marl:hasPolarityValue') + polarityValue = alias('marl:polarityValue') class Error(BaseModel, Exception): @@ -301,59 +328,121 @@ class Error(BaseModel, Exception): return Exception.__hash__(self) -# Add the remaining schemas programmatically +class AggregatedEvaluation(BaseModel): + schema = 'aggregatedEvaluation' -def _class_from_schema(name, schema=None, schema_file=None, base_classes=None): - base_classes = base_classes or [] - base_classes.append(BaseModel) - attrs = {} - if schema: - attrs['schema'] = schema - elif schema_file: - attrs['schema_file'] = schema_file - else: - attrs['schema'] = name - name = "".join((name[0].upper(), name[1:])) - return BaseMeta(name, base_classes, attrs) + evaluations = alias('senpy:evaluations', []) -def _add_class_from_schema(*args, **kwargs): - generatedClass = _class_from_schema(*args, **kwargs) - globals()[generatedClass.__name__] = generatedClass - del generatedClass +class Dataset(BaseModel): + schema = 'dataset' + + +class Datasets(BaseModel): + schema = 'datasets' + + datasets = [] + + +class Emotion(BaseModel): + schema = 'emotion' + + +class EmotionConversion(BaseModel): + schema = 'emotionConversion' + + +class EmotionConversionPlugin(BaseModel): + schema = 'emotionConversionPlugin' + + +class EmotionAnalysis(BaseModel): + schema = 'emotionAnalysis' + + +class EmotionModel(BaseModel): + schema = 'emotionModel' + onyx__hasEmotionCategory = [] + + +class EmotionPlugin(BaseModel): + schema = 'emotionPlugin' + + +class EmotionSet(BaseModel): + schema = 'emotionSet' + + onyx__hasEmotion = [] + + +class Evaluation(BaseModel): + schema = 'evaluation' + + metrics = alias('senpy:metrics', []) + + +class Entity(BaseModel): + schema = 'entity' + +class Help(BaseModel): + schema = 'help' -for i in [ - 'aggregatedEvaluation', - 'dataset', - 'datasets', - 'emotion', - 'emotionConversion', - 'emotionConversionPlugin', - 'emotionAnalysis', - 'emotionModel', - 'emotionPlugin', - 'emotionSet', - 'evaluation', - 'entity', - 'help', - 'metric', - 'parameter', - 'plugins', - 'response', - 'results', - 'sentimentPlugin', - 'suggestion', - 'topic', - -]: - _add_class_from_schema(i) + +class Metric(BaseModel): + schema = 'metric' + + +class Parameter(BaseModel): + schema = 'parameter' + + +class Plugins(BaseModel): + schema = 'plugins' + + plugins = [] + + +class Response(BaseModel): + schema = 'response' + + +class Results(BaseModel): + schema = 'results' + + _terse_keys = ['entries', ] + + activities = [] + entries = [] + + def activity(self, id): + for i in self.activities: + if i.id == id: + return i + return None + + +class SentimentPlugin(BaseModel): + schema = 'sentimentPlugin' + + +class Suggestion(BaseModel): + schema = 'suggestion' + + +class Topic(BaseModel): + schema = 'topic' class Analysis(BaseModel): + ''' + A prov:Activity that results of executing a Plugin on an entry with a set of + parameters. + ''' schema = 'analysis' - parameters = alias('prov:used') + parameters = alias('prov:used', []) + algorithm = alias('prov:wasAssociatedWith', []) @property def params(self): @@ -373,9 +462,11 @@ class Analysis(BaseModel): else: self.parameters.append(Parameter(name=k, value=v)) # noqa: F821 - @property - def algorithm(self): - return self['prov:wasAssociatedWith'] + def param(self, key, default=None): + for param in self.parameters: + if param['name'] == key: + return param['value'] + return default @property def plugin(self): @@ -387,15 +478,39 @@ class Analysis(BaseModel): self['prov:wasAssociatedWith'] = value.id def run(self, request): - return self.plugin.process(request, self.params) + return self.plugin.process(request, self) class Plugin(BaseModel): schema = 'plugin' + extra_params = {} - def activity(self, parameters): - '''Generate a prov:Activity from this plugin and the ''' + def activity(self, parameters=None): + '''Generate an Analysis (prov:Activity) from this plugin and the given parameters''' a = Analysis() a.plugin = self - a.params = parameters + if parameters: + a.params = parameters return a + + +# More classes could be added programmatically + +def _class_from_schema(name, schema=None, schema_file=None, base_classes=None): + base_classes = base_classes or [] + base_classes.append(BaseModel) + attrs = {} + if schema: + attrs['schema'] = schema + elif schema_file: + attrs['schema_file'] = schema_file + else: + attrs['schema'] = name + name = "".join((name[0].upper(), name[1:])) + return BaseMeta(name, base_classes, attrs) + + +def _add_class_from_schema(*args, **kwargs): + generatedClass = _class_from_schema(*args, **kwargs) + globals()[generatedClass.__name__] = generatedClass + del generatedClass diff --git a/senpy/plugins/__init__.py b/senpy/plugins/__init__.py index 70f5bdd..79a97c9 100644 --- a/senpy/plugins/__init__.py +++ b/senpy/plugins/__init__.py @@ -1,3 +1,5 @@ +#!/usr/local/bin/python +# -*- coding: utf-8 -*- from future import standard_library standard_library.install_aliases() @@ -17,7 +19,12 @@ import subprocess import importlib import yaml import threading +import multiprocessing +import pkg_resources from nltk import download +from textwrap import dedent +from sklearn.base import TransformerMixin, BaseEstimator +from itertools import product from .. import models, utils from .. import api @@ -31,17 +38,19 @@ class PluginMeta(models.BaseMeta): _classes = {} def __new__(mcs, name, bases, attrs, **kwargs): - plugin_type = [] - if hasattr(bases[0], 'plugin_type'): - plugin_type += bases[0].plugin_type - plugin_type.append(name) - alias = attrs.get('name', name) - attrs['plugin_type'] = plugin_type + plugin_type = set() + for base in bases: + if hasattr(base, '_plugin_type'): + plugin_type |= base._plugin_type + plugin_type.add(name) + alias = attrs.get('name', name).lower() + attrs['_plugin_type'] = plugin_type + logger.debug('Adding new plugin class', name, bases, attrs, plugin_type) attrs['name'] = alias if 'description' not in attrs: doc = attrs.get('__doc__', None) if doc: - attrs['description'] = doc + attrs['description'] = dedent(doc) else: logger.warning( ('Plugin {} does not have a description. ' @@ -77,6 +86,9 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)): ''' + _terse_keys = ['name', '@id', '@type', 'author', 'description', + 'extra_params', 'is_activated', 'url', 'version'] + def __init__(self, info=None, data_folder=None, **kwargs): """ Provides a canonical name for plugins and serves as base for other @@ -126,33 +138,42 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)): def get_folder(self): return os.path.dirname(inspect.getfile(self.__class__)) + def _activate(self): + self.activate() + self.is_activated = True + + def _deactivate(self): + self.is_activated = False + self.deactivate() + def activate(self): pass def deactivate(self): pass - def process(self, request, parameters, **kwargs): + def process(self, request, activity, **kwargs): """ An implemented plugin should override this method. - Here, we assume that a process_entries method exists.""" + Here, we assume that a process_entries method exists. + """ newentries = list( - self.process_entries(request.entries, parameters)) + self.process_entries(request.entries, activity)) request.entries = newentries return request - def process_entries(self, entries, parameters): + def process_entries(self, entries, activity): for entry in entries: self.log.debug('Processing entry with plugin {}: {}'.format( self, entry)) - results = self.process_entry(entry, parameters) + results = self.process_entry(entry, activity) if inspect.isgenerator(results): for result in results: yield result else: yield results - def process_entry(self, entry, parameters): + def process_entry(self, entry, activity): """ This base method is here to adapt plugins which only implement the *process* function. @@ -173,9 +194,12 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)): test_cases = self.test_cases for case in test_cases: try: + fmt = 'case: {}'.format(case.get('name', case)) + if 'name' in case: + self.log.info('Test case: {}'.format(case['name'])) + self.log.debug('Test case:\n\t{}'.format( + pprint.pformat(fmt))) self.test_case(case) - self.log.debug('Test case passed:\n{}'.format( - pprint.pformat(case))) except Exception as ex: self.log.warning('Test case failed:\n{}'.format( pprint.pformat(case))) @@ -200,7 +224,9 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)): entry, ] - method = partial(self.process, request, parameters) + activity = self.activity(parameters) + + method = partial(self.process, request, activity) if mock: res = method() @@ -243,34 +269,41 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)): SenpyPlugin = Plugin -class Analysis(Plugin): +class Analyser(Plugin): ''' A subclass of Plugin that analyses text and provides an annotation. ''' - def analyse(self, request, parameters): - return super(Analysis, self).process(request, parameters) + # Deprecated + def analyse(self, request, activity): + return super(Analyser, self).process(request, activity) - def analyse_entries(self, entries, parameters): - for i in super(Analysis, self).process_entries(entries, parameters): + # Deprecated + def analyse_entries(self, entries, activity): + for i in super(Analyser, self).process_entries(entries, activity): yield i - def process(self, request, parameters, **kwargs): - return self.analyse(request, parameters) + def process(self, request, activity, **kwargs): + return self.analyse(request, activity) - def process_entries(self, entries, parameters): - for i in self.analyse_entries(entries, parameters): + def process_entries(self, entries, activity): + for i in self.analyse_entries(entries, activity): yield i - def process_entry(self, entry, parameters, **kwargs): + def process_entry(self, entry, activity, **kwargs): if hasattr(self, 'analyse_entry'): - for i in self.analyse_entry(entry, parameters): + for i in self.analyse_entry(entry, activity): yield i else: - super(Analysis, self).process_entry(entry, parameters, **kwargs) + super(Analyser, self).process_entry(entry, activity, **kwargs) + +AnalysisPlugin = Analyser -AnalysisPlugin = Analysis + +class Transformation(AnalysisPlugin): + '''Empty''' + pass class Conversion(Plugin): @@ -297,32 +330,79 @@ class Conversion(Plugin): ConversionPlugin = Conversion -class SentimentPlugin(Analysis, models.SentimentPlugin): +class Evaluable(Plugin): + ''' + Common class for plugins that can be evaluated with GSITK. + + They should implement the methods below. + ''' + + def as_pipe(self): + raise Exception('Implement the as_pipe function') + + def evaluate_func(self, X, activity=None): + raise Exception('Implement the evaluate_func function') + + +class SentimentPlugin(Analyser, Evaluable, models.SentimentPlugin): ''' Sentiment plugins provide sentiment annotation (using Marl) ''' minPolarityValue = 0 maxPolarityValue = 1 + _terse_keys = Analyser._terse_keys + ['minPolarityValue', 'maxPolarityValue'] + def test_case(self, case): if 'polarity' in case: expected = case.get('expected', {}) s = models.Sentiment(_auto_id=False) s.marl__hasPolarity = case['polarity'] - if 'sentiments' not in expected: - expected['sentiments'] = [] - expected['sentiments'].append(s) + if 'marl:hasOpinion' not in expected: + expected['marl:hasOpinion'] = [] + expected['marl:hasOpinion'].append(s) case['expected'] = expected super(SentimentPlugin, self).test_case(case) + def normalize(self, value, minValue, maxValue): + nv = minValue + (value - self.minPolarityValue) * ( + self.maxPolarityValue - self.minPolarityValue) / (maxValue - minValue) + return nv + + def as_pipe(self): + pipe = gsitk_compat.Pipeline([('senpy-plugin', ScikitWrapper(self))]) + pipe.name = self.id + return pipe -class EmotionPlugin(Analysis, models.EmotionPlugin): + def evaluate_func(self, X, activity=None): + if activity is None: + parameters = api.parse_params({}, + self.extra_params) + activity = self.activity(parameters) + entries = [] + for feat in X: + entries.append(models.Entry(nif__isString=feat[0])) + labels = [] + for e in self.process_entries(entries, activity): + sent = e.sentiments[0].polarity + label = -1 + if sent == 'marl:Positive': + label = 1 + elif sent == 'marl:Negative': + label = -1 + labels.append(label) + return labels + + +class EmotionPlugin(Analyser, models.EmotionPlugin): ''' Emotion plugins provide emotion annotation (using Onyx) ''' minEmotionValue = 0 maxEmotionValue = 1 + _terse_keys = Analyser._terse_keys + ['minEmotionValue', 'maxEmotionValue'] + class EmotionConversion(Conversion): ''' @@ -345,69 +425,67 @@ EmotionConversionPlugin = EmotionConversion class PostProcessing(Plugin): + ''' + A plugin that converts the output of other plugins (post-processing). + ''' def check(self, request, plugins): '''Should this plugin be run for this request?''' return False -class Box(AnalysisPlugin): +class Box(Analyser): ''' Black box plugins delegate analysis to a function. - The flow is like so: + The flow is like this: .. code-block:: - entry --> input() --> predict_one() --> output() --> entry' + entries --> to_features() --> predict_many() --> to_entry() --> entries' - In other words: their ``input`` method convers a query (entry and a set of parameters) into - the input to the box method. The ``output`` method convers the results given by the box into - an entry that senpy can handle. + In other words: their ``to_features`` method converts a query (entry and a set of parameters) + into the input to the `predict_one` method, which only uses an array of features. + The ``to_entry`` method converts the results given by the box into an entry that senpy can + handle. ''' - def input(self, entry, params=None): + def to_features(self, entry, activity=None): '''Transforms a query (entry+param) into an input for the black box''' return entry - def output(self, output, entry=None, params=None): + def to_entry(self, features, entry=None, activity=None): '''Transforms the results of the black box into an entry''' - return output + return entry - def predict_one(self, input): + def predict_one(self, features, activity=None): raise NotImplementedError( 'You should define the behavior of this plugin') - def process_entries(self, entries, params): - for entry in entries: - input = self.input(entry=entry, params=params) - results = self.predict_one(input=input) - yield self.output(output=results, entry=entry, params=params) + def predict_many(self, features, activity=None): + results = [] + for feat in features: + results.append(self.predict_one(features=feat, activity=activity)) + return results - def fit(self, X=None, y=None): - return self - - def transform(self, X): - return [self.predict_one(x) for x in X] - - def predict(self, X): - return self.transform(X) + def process_entry(self, entry, activity): + for i in self.process_entries([entry], activity): + yield i - def fit_transform(self, X, y): - self.fit(X, y) - return self.transform(X) + def process_entries(self, entries, activity): + features = [] + for entry in entries: + features.append(self.to_features(entry=entry, activity=activity)) + results = self.predict_many(features=features, activity=activity) - def as_pipe(self): - pipe = gsitk_compat.Pipeline([('plugin', self)]) - pipe.name = self.name - return pipe + for (result, entry) in zip(results, entries): + yield self.to_entry(features=result, entry=entry, activity=activity) class TextBox(Box): '''A black box plugin that takes only text as input''' - def input(self, entry, params): - entry = super(TextBox, self).input(entry, params) - return entry['nif:isString'] + def to_features(self, entry, activity): + return [entry['nif:isString']] class SentimentBox(TextBox, SentimentPlugin): @@ -415,17 +493,35 @@ class SentimentBox(TextBox, SentimentPlugin): A box plugin where the output is only a polarity label or a tuple (polarity, polarityValue) ''' - def output(self, output, entry, **kwargs): - s = models.Sentiment() - try: - label, value = output - except ValueError: - label, value = output, None - s.prov(self) - s.polarity = label - if value is not None: - s.polarityValue = value - entry.sentiments.append(s) + classes = ['marl:Positive', 'marl:Neutral', 'marl:Negative'] + binary = True + + def to_entry(self, features, entry, activity, **kwargs): + + if len(features) != len(self.classes): + raise models.Error('The number of features ({}) does not match the classes ' + '(plugin.classes ({})'.format(len(features), len(self.classes))) + + minValue = activity.param('marl:minPolarityValue', 0) + maxValue = activity.param('marl:minPolarityValue', 1) + activity['marl:minPolarityValue'] = minValue + activity['marl:maxPolarityValue'] = maxValue + + for k, v in zip(self.classes, features): + s = models.Sentiment() + if self.binary: + if not v: # Carry on if the value is 0 + continue + s['marl:hasPolarity'] = k + else: + if v is not None: + s['marl:hasPolarity'] = k + nv = self.normalize(v, minValue, maxValue) + s['marl:polarityValue'] = nv + s.prov(activity) + + entry.sentiments.append(s) + return entry @@ -434,14 +530,23 @@ class EmotionBox(TextBox, EmotionPlugin): A box plugin where the output is only an a tuple of emotion labels ''' - def output(self, output, entry, **kwargs): - if not isinstance(output, list): - output = [output] + EMOTIONS = [] + with_intensity = True + + def to_entry(self, features, entry, activity, **kwargs): s = models.EmotionSet() - entry.emotions.append(s) - for label in output: + + if len(features) != len(self.EMOTIONS): + raise Exception(('The number of classes in the plugin and the number of features ' + 'do not match')) + + for label, intensity in zip(self.EMOTIONS, features): e = models.Emotion(onyx__hasEmotionCategory=label) - s.append(e) + if self.with_intensity: + e.onyx__hasEmotionIntensity = intensity + s.onyx__hasEmotion.append(e) + s.prov(activity) + entry.emotions.append(s) return entry @@ -454,11 +559,15 @@ class MappingMixin(object): def mappings(self, value): self._mappings = value - def output(self, output, entry, params): - output = self.mappings.get(output, self.mappings.get( - 'default', output)) - return super(MappingMixin, self).output( - output=output, entry=entry, params=params) + def to_entry(self, features, entry, activity): + features = list(features) + for i, feat in enumerate(features): + features[i] = self.mappings.get(feat, + self.mappings.get('default', + feat)) + return super(MappingMixin, self).to_entry(features=features, + entry=entry, + activity=activity) class ShelfMixin(object): @@ -505,7 +614,7 @@ class ShelfMixin(object): pickle.dump(self._sh, f) -def pfilter(plugins, plugin_type=Analysis, **kwargs): +def pfilter(plugins, plugin_type=Analyser, **kwargs): """ Filter plugins by different criteria """ if isinstance(plugins, models.Plugins): plugins = plugins.plugins @@ -526,6 +635,9 @@ def pfilter(plugins, plugin_type=Analysis, **kwargs): else: candidates = plugins + if 'name' in kwargs: + kwargs['name'] = kwargs['name'].lower() + logger.debug(candidates) def matches(plug): @@ -549,31 +661,48 @@ def load_module(name, root=None): def _log_subprocess_output(process): for line in iter(process.stdout.readline, b''): - logger.info('%r', line) + logger.info('%s', line.decode()) for line in iter(process.stderr.readline, b''): - logger.error('%r', line) + logger.error('%s', line.decode()) + + +def missing_requirements(reqs): + queue = [] + pool = multiprocessing.Pool(4) + for req in reqs: + res = pool.apply_async(pkg_resources.get_distribution, (req,)) + queue.append((req, res)) + missing = [] + for req, job in queue: + try: + job.get(1) + except Exception: + missing.append(req) + return missing def install_deps(*plugins): installed = False nltk_resources = set() + requirements = [] for info in plugins: requirements = info.get('requirements', []) if requirements: - pip_args = [sys.executable, '-m', 'pip', 'install'] - for req in requirements: - pip_args.append(req) - logger.info('Installing requirements: ' + str(requirements)) - process = subprocess.Popen( - pip_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - _log_subprocess_output(process) - exitcode = process.wait() - installed = True - if exitcode != 0: - raise models.Error( - "Dependencies not properly installed: {}".format(pip_args)) + requirements += missing_requirements(requirements) nltk_resources |= set(info.get('nltk_resources', [])) - + if requirements: + logger.info('Installing requirements: ' + str(requirements)) + pip_args = [sys.executable, '-m', 'pip', 'install'] + for req in requirements: + pip_args.append(req) + process = subprocess.Popen( + pip_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + _log_subprocess_output(process) + exitcode = process.wait() + installed = True + if exitcode != 0: + raise models.Error( + "Dependencies not properly installed: {}".format(pip_args)) installed |= download(list(nltk_resources)) return installed @@ -632,7 +761,7 @@ def from_info(info, root=None, install_on_fail=True, **kwargs): def parse_plugin_info(fpath): logger.debug("Parsing plugin info: {}".format(fpath)) with open(fpath, 'r') as f: - info = yaml.load(f) + info = yaml.load(f, Loader=yaml.FullLoader) info['_path'] = fpath return info @@ -688,14 +817,34 @@ def _from_loaded_module(module, info=None, **kwargs): yield instance +cached_evs = {} + + def evaluate(plugins, datasets, **kwargs): - ev = gsitk_compat.Eval( - tuples=None, - datasets=datasets, - pipelines=[plugin.as_pipe() for plugin in plugins]) - ev.evaluate() - results = ev.results - evaluations = evaluations_to_JSONLD(results, **kwargs) + for plug in plugins: + if not hasattr(plug, 'as_pipe'): + raise models.Error('Plugin {} cannot be evaluated'.format(plug.name)) + + tuples = list(product(plugins, datasets)) + missing = [] + for (p, d) in tuples: + if (p.id, d) not in cached_evs: + pipe = p.as_pipe() + missing.append(gsitk_compat.EvalPipeline(pipe, d)) + if missing: + ev = gsitk_compat.Eval(tuples=missing, datasets=datasets) + ev.evaluate() + results = ev.results + new_ev = evaluations_to_JSONLD(results, **kwargs) + for ev in new_ev: + dataset = ev.evaluatesOn + model = ev.evaluates.rstrip('__' + dataset) + cached_evs[(model, dataset)] = ev + evaluations = [] + print(tuples, 'Cached evs', cached_evs) + for (p, d) in tuples: + print('Adding', d, p) + evaluations.append(cached_evs[(p.id, d)]) return evaluations @@ -708,7 +857,7 @@ def evaluations_to_JSONLD(results, flatten=False): metric_names = ['accuracy', 'precision_macro', 'recall_macro', 'f1_macro', 'f1_weighted', 'f1_micro', 'f1_macro'] - for index, row in results.iterrows(): + for index, row in results.fillna('Not Available').iterrows(): evaluation = models.Evaluation() if row.get('CV', True): evaluation['@type'] = ['StaticCV', 'Evaluation'] @@ -724,10 +873,29 @@ def evaluations_to_JSONLD(results, flatten=False): # We should probably discontinue this representation for name in metric_names: metric = models.Metric() - metric['@id'] = 'Metric' + str(i) metric['@type'] = name.capitalize() metric.value = row[name] evaluation.metrics.append(metric) i += 1 evaluations.append(evaluation) return evaluations + + +class ScikitWrapper(BaseEstimator, TransformerMixin): + def __init__(self, plugin=None): + self.plugin = plugin + + def fit(self, X=None, y=None): + if self.plugin is not None and not self.plugin.is_activated: + self.plugin.activate() + return self + + def transform(self, X): + return self.plugin.evaluate_func(X, None) + + def predict(self, X): + return self.transform(X) + + def fit_transform(self, X, y): + self.fit(X, y) + return self.transform(X) diff --git a/senpy/plugins/example/emoRand/emoRand.py b/senpy/plugins/example/emoRand/emoRand.py deleted file mode 100644 index 25327c4..0000000 --- a/senpy/plugins/example/emoRand/emoRand.py +++ /dev/null @@ -1,34 +0,0 @@ -import random - -from senpy.plugins import EmotionPlugin -from senpy.models import EmotionSet, Emotion, Entry - - -class EmoRand(EmotionPlugin): - name = "emoRand" - description = 'A sample plugin that returns a random emotion annotation' - author = '@balkian' - version = '0.1' - url = "https://github.com/gsi-upm/senpy-plugins-community" - requirements = {} - onyx__usesEmotionModel = "emoml:big6" - - def analyse_entry(self, entry, params): - category = "emoml:big6happiness" - number = max(-1, min(1, random.gauss(0, 0.5))) - if number > 0: - category = "emoml:big6anger" - emotionSet = EmotionSet() - emotion = Emotion({"onyx:hasEmotionCategory": category}) - emotionSet.onyx__hasEmotion.append(emotion) - emotionSet.prov__wasGeneratedBy = self.id - entry.emotions.append(emotionSet) - yield entry - - def test(self): - params = dict() - results = list() - for i in range(100): - res = next(self.analyse_entry(Entry(nif__isString="Hello"), params)) - res.validate() - results.append(res.emotions[0]['onyx:hasEmotion'][0]['onyx:hasEmotionCategory']) diff --git a/senpy/plugins/misc/split_plugin.py b/senpy/plugins/misc/split_plugin.py index d54a6fb..af13d65 100644 --- a/senpy/plugins/misc/split_plugin.py +++ b/senpy/plugins/misc/split_plugin.py @@ -1,19 +1,26 @@ -from senpy.plugins import AnalysisPlugin +from senpy.plugins import Transformation from senpy.models import Entry from nltk.tokenize.punkt import PunktSentenceTokenizer from nltk.tokenize.simple import LineTokenizer -import nltk -class Split(AnalysisPlugin): - '''description: A sample plugin that chunks input text''' +class Split(Transformation): + ''' + A plugin that chunks input text, into paragraphs or sentences. + + It does not provide any sort of annotation, and it is meant to precede + other annotation plugins, when the annotation of individual sentences + (or paragraphs) is required. + ''' author = ["@militarpancho", '@balkian'] version = '0.3' url = "https://github.com/gsi-upm/senpy" + nltk_resources = ['punkt'] extra_params = { 'delimiter': { + 'description': 'Split text into paragraphs or sentences.', 'aliases': ['type', 't'], 'required': False, 'default': 'sentence', @@ -21,12 +28,9 @@ class Split(AnalysisPlugin): }, } - def activate(self): - nltk.download('punkt') - - def analyse_entry(self, entry, params): + def analyse_entry(self, entry, activity): yield entry - chunker_type = params["delimiter"] + chunker_type = activity.params["delimiter"] original_text = entry['nif:isString'] if chunker_type == "sentence": tokenizer = PunktSentenceTokenizer() diff --git a/senpy/plugins/postprocessing/emotion/centroids.py b/senpy/plugins/postprocessing/emotion/centroids.py index eae28d6..492b389 100644 --- a/senpy/plugins/postprocessing/emotion/centroids.py +++ b/senpy/plugins/postprocessing/emotion/centroids.py @@ -103,7 +103,9 @@ class CentroidConversion(EmotionConversionPlugin): for i in emotionSet.onyx__hasEmotion: e.onyx__hasEmotion.append(self._backwards_conversion(i)) else: - raise Error('EMOTION MODEL NOT KNOWN') + raise Error('EMOTION MODEL NOT KNOWN. ' + 'Cannot convert from {} to {}'.format(fromModel, + toModel)) yield e def test(self, info=None): diff --git a/senpy/plugins/postprocessing/emotion/ekman2vad.senpy b/senpy/plugins/postprocessing/emotion/ekman2vad.senpy index 90fcf0a..d089a41 100644 --- a/senpy/plugins/postprocessing/emotion/ekman2vad.senpy +++ b/senpy/plugins/postprocessing/emotion/ekman2vad.senpy @@ -31,7 +31,7 @@ centroids_direction: - emoml:pad aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times A: emoml:pad-dimensions:arousal - V: emoml:pad-dimensions:pleasure + V: emoml:pad-dimensions:valence D: emoml:pad-dimensions:dominance anger: emoml:big6anger disgust: emoml:big6disgust diff --git a/senpy/plugins/postprocessing/emotion/maxEmotion_plugin.py b/senpy/plugins/postprocessing/emotion/maxEmotion_plugin.py index 6e3de7f..a024d55 100644 --- a/senpy/plugins/postprocessing/emotion/maxEmotion_plugin.py +++ b/senpy/plugins/postprocessing/emotion/maxEmotion_plugin.py @@ -6,7 +6,7 @@ class MaxEmotion(PostProcessing): author = '@dsuarezsouto' version = '0.1' - def process_entry(self, entry, params): + def process_entry(self, entry, activity): if len(entry.emotions) < 1: yield entry return @@ -32,7 +32,7 @@ class MaxEmotion(PostProcessing): entry.emotions[0]['onyx:hasEmotion'] = [max_emotion] - entry.emotions[0]['prov:wasGeneratedBy'] = "maxSentiment" + entry.emotions[0]['prov:wasGeneratedBy'] = activity.id yield entry def check(self, request, plugins): @@ -43,12 +43,11 @@ class MaxEmotion(PostProcessing): # 2 Case to return a Neutral Emotion. test_cases = [ { - "name": - "If there are several emotions within an emotion set, reduce it to one.", + "name": "If there are several emotions within an emotion set, reduce it to one.", "entry": { "@type": "entry", - "emotions": [ + "onyx:hasEmotionSet": [ { "@id": "Emotions0", @@ -94,7 +93,7 @@ class MaxEmotion(PostProcessing): 'expected': { "@type": "entry", - "emotions": [ + "onyx:hasEmotionSet": [ { "@id": "Emotions0", @@ -107,9 +106,7 @@ class MaxEmotion(PostProcessing): "onyx:hasEmotionCategory": "joy", "onyx:hasEmotionIntensity": 0.3333333333333333 } - ], - "prov:wasGeneratedBy": - 'maxSentiment' + ] } ], "nif:isString": @@ -122,7 +119,7 @@ class MaxEmotion(PostProcessing): "entry": { "@type": "entry", - "emotions": [{ + "onyx:hasEmotionSet": [{ "@id": "Emotions0", "@type": @@ -171,7 +168,7 @@ class MaxEmotion(PostProcessing): 'expected': { "@type": "entry", - "emotions": [{ + "onyx:hasEmotionSet": [{ "@id": "Emotions0", "@type": @@ -181,9 +178,7 @@ class MaxEmotion(PostProcessing): "@type": "emotion", "onyx:hasEmotionCategory": "neutral", "onyx:hasEmotionIntensity": 1 - }], - "prov:wasGeneratedBy": - 'maxSentiment' + }] }], "nif:isString": "Test" diff --git a/senpy/plugins/sentiment/sentiment140/sentiment140_plugin.py b/senpy/plugins/sentiment/sentiment140/sentiment140_plugin.py index d40fd15..1b7c5ce 100644 --- a/senpy/plugins/sentiment/sentiment140/sentiment140_plugin.py +++ b/senpy/plugins/sentiment/sentiment140/sentiment140_plugin.py @@ -1,13 +1,12 @@ import requests import json -from senpy.plugins import SentimentPlugin -from senpy.models import Sentiment +from senpy.plugins import SentimentBox ENDPOINT = 'http://www.sentiment140.com/api/bulkClassifyJson' -class Sentiment140(SentimentPlugin): +class Sentiment140(SentimentBox): '''Connects to the sentiment140 free API: http://sentiment140.com''' author = "@balkian" @@ -16,43 +15,40 @@ class Sentiment140(SentimentPlugin): extra_params = { 'language': { "@id": 'lang_sentiment140', + 'description': 'language of the text', 'aliases': ['language', 'l'], - 'required': False, + 'required': True, 'default': 'auto', 'options': ['es', 'en', 'auto'] } } - maxPolarityValue = 1 - minPolarityValue = 0 + classes = ['marl:Positive', 'marl:Neutral', 'marl:Negative'] + binary = True + + def predict_many(self, features, activity): + lang = activity.params["language"] + data = [] + + for feature in features: + data.append({'text': feature[0]}) - def analyse_entry(self, entry, params): - lang = params["language"] res = requests.post(ENDPOINT, json.dumps({ "language": lang, - "data": [{ - "text": entry['nif:isString'] - }] + "data": data })) - p = params.get("prefix", None) - polarity_value = self.maxPolarityValue * int( - res.json()["data"][0]["polarity"]) * 0.25 - polarity = "marl:Neutral" - neutral_value = self.maxPolarityValue / 2.0 - if polarity_value > neutral_value: - polarity = "marl:Positive" - elif polarity_value < neutral_value: - polarity = "marl:Negative" - sentiment = Sentiment( - prefix=p, - marl__hasPolarity=polarity, - marl__polarityValue=polarity_value) - sentiment.prov__wasGeneratedBy = self.id - entry.sentiments.append(sentiment) - entry.language = lang - yield entry + for res in res.json()["data"]: + polarity = int(res['polarity']) + neutral_value = 2 + if polarity > neutral_value: + yield [1, 0, 0] + continue + elif polarity < neutral_value: + yield [0, 0, 1] + continue + yield [0, 1, 0] test_cases = [ { @@ -62,7 +58,7 @@ class Sentiment140(SentimentPlugin): 'params': {}, 'expected': { "nif:isString": "I love Titanic", - 'sentiments': [ + 'marl:hasOpinion': [ { 'marl:hasPolarity': 'marl:Positive', } diff --git a/senpy/schemas/aggregatedEvaluation.json b/senpy/schemas/aggregatedEvaluation.json index 4560aab..bbeb2d0 100644 --- a/senpy/schemas/aggregatedEvaluation.json +++ b/senpy/schemas/aggregatedEvaluation.json @@ -11,14 +11,12 @@ "$ref": "context.json" }, "@type": { - "default": "AggregatedEvaluation" }, "@id": { "description": "ID of the aggregated evaluation", "type": "string" }, "evaluations": { - "default": [], "type": "array", "items": { "anyOf": [ diff --git a/senpy/schemas/analysis.json b/senpy/schemas/analysis.json index df4c42a..5e9541d 100644 --- a/senpy/schemas/analysis.json +++ b/senpy/schemas/analysis.json @@ -17,7 +17,6 @@ "prov:used": { "description": "Parameters of the algorithm", "@type": "array", - "default": [], "type": "array", "items": { "$ref": "parameter.json" diff --git a/senpy/schemas/context.jsonld b/senpy/schemas/context.jsonld index bebc6f4..7194262 100644 --- a/senpy/schemas/context.jsonld +++ b/senpy/schemas/context.jsonld @@ -1,8 +1,8 @@ { "@context": { - "@vocab": "http://www.gsi.dit.upm.es/ontologies/senpy#", + "@vocab": "http://www.gsi.upm.es/onto/senpy/ns#", "dc": "http://dublincore.org/2012/06/14/dcelements#", - "me": "http://www.mixedemotions-project.eu/ns/model#", + "senpy": "http://www.gsi.upm.es/onto/senpy/ns#", "prov": "http://www.w3.org/ns/prov#", "nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#", "marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#", @@ -16,10 +16,10 @@ "@container": "@set" }, "entities": { - "@id": "me:hasEntities" + "@id": "senpy:hasEntities" }, "suggestions": { - "@id": "me:hasSuggestions", + "@id": "senpy:hasSuggestions", "@container": "@set" }, "onyx:hasEmotion": { @@ -40,7 +40,7 @@ "@id": "prov:used", "@container": "@set" }, - "analysis": { + "activities": { "@id": "prov:wasInformedBy", "@type": "@id", "@container": "@set" @@ -65,6 +65,13 @@ }, "onyx:conversionTo": { "@type": "@id" - } + }, + "parameters": { + "@type": "Parameter" + }, + "errors": { + "@type": "ParameterError" + }, + "prefix": "http://senpy.invalid/" } } diff --git a/senpy/schemas/datasets.json b/senpy/schemas/datasets.json index 98bec82..6afda8e 100644 --- a/senpy/schemas/datasets.json +++ b/senpy/schemas/datasets.json @@ -7,7 +7,6 @@ "properties": { "datasets": { "type": "array", - "default": [], "items": { "$ref": "dataset.json" } diff --git a/senpy/schemas/emotionModel.json b/senpy/schemas/emotionModel.json index 437c732..b9d1cb1 100644 --- a/senpy/schemas/emotionModel.json +++ b/senpy/schemas/emotionModel.json @@ -19,8 +19,7 @@ "type": "array", "items": { "$ref": "emotion.json" - }, - "default": [] + } } }, "required": ["@id", "onyx:hasEmotion"] diff --git a/senpy/schemas/emotionSet.json b/senpy/schemas/emotionSet.json index 6e8be0b..e953911 100644 --- a/senpy/schemas/emotionSet.json +++ b/senpy/schemas/emotionSet.json @@ -12,8 +12,7 @@ "type": "array", "items": { "$ref": "emotion.json" - }, - "default": [] + } }, "prov:wasGeneratedBy": { "type": "string", diff --git a/senpy/schemas/entry.json b/senpy/schemas/entry.json index 406f69a..940925a 100644 --- a/senpy/schemas/entry.json +++ b/senpy/schemas/entry.json @@ -9,30 +9,13 @@ "description": "String contained in this Context. Alternative: nif:isString", "type": "string" }, - "sentiments": { + "marl:hasOpinion": { "type": "array", - "items": {"$ref": "sentiment.json" }, - "default": [] + "items": {"$ref": "sentiment.json" } }, - "emotions": { + "onyx:hasEmotionSet": { "type": "array", - "items": {"$ref": "emotionSet.json" }, - "default": [] - }, - "entities": { - "type": "array", - "items": {"$ref": "entity.json" }, - "default": [] - }, - "topics": { - "type": "array", - "items": {"$ref": "topic.json" }, - "default": [] - }, - "suggestions": { - "type": "array", - "items": {"$ref": "suggestion.json" }, - "default": [] + "items": {"$ref": "emotionSet.json" } } }, "required": ["nif:isString"] diff --git a/senpy/schemas/evaluation.json b/senpy/schemas/evaluation.json index 5af72d8..aad5881 100644 --- a/senpy/schemas/evaluation.json +++ b/senpy/schemas/evaluation.json @@ -6,8 +6,7 @@ "type": "string" }, "@type": { - "type": "array", - "default": "Evaluation" + "type": "array" }, "metrics": { diff --git a/senpy/schemas/plugin.json b/senpy/schemas/plugin.json index f15f3f3..ee6d275 100644 --- a/senpy/schemas/plugin.json +++ b/senpy/schemas/plugin.json @@ -24,8 +24,7 @@ "description": "Sub-type of plugin. e.g. sentimentPlugin" }, "extra_params": { - "type": "object", - "default": {} + "type": "object" } } } diff --git a/senpy/schemas/plugins.json b/senpy/schemas/plugins.json index df2aa3b..0e360d4 100644 --- a/senpy/schemas/plugins.json +++ b/senpy/schemas/plugins.json @@ -7,9 +7,15 @@ "properties": { "plugins": { "type": "array", - "default": [], "items": { - "$ref": "plugin.json" + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "plugin.json" + } + ] } } } diff --git a/senpy/schemas/results.json b/senpy/schemas/results.json index 48cf335..f6ddadd 100644 --- a/senpy/schemas/results.json +++ b/senpy/schemas/results.json @@ -1,5 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", + "name": "Entry", "allOf": [ {"$ref": "response.json"}, { @@ -10,15 +11,11 @@ "@context": { "$ref": "context.json" }, - "@type": { - "default": "results" - }, "@id": { "description": "ID of the analysis", "type": "string" }, - "analysis": { - "default": [], + "activities": { "type": "array", "items": { "$ref": "analysis.json" @@ -26,14 +23,13 @@ }, "entries": { "type": "array", - "default": [], "items": { "$ref": "entry.json" } } }, - "required": ["@id", "analysis", "entries"] + "required": ["@id", "activities", "entries"] } ] } diff --git a/senpy/static/css/bootstrap-theme.css b/senpy/static/css/bootstrap-theme.css deleted file mode 100644 index a406992..0000000 --- a/senpy/static/css/bootstrap-theme.css +++ /dev/null @@ -1,347 +0,0 @@ -/*! - * Bootstrap v3.1.1 (http://getbootstrap.com) - * Copyright 2011-2014 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -.btn-default, -.btn-primary, -.btn-success, -.btn-info, -.btn-warning, -.btn-danger { - text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); -} -.btn-default:active, -.btn-primary:active, -.btn-success:active, -.btn-info:active, -.btn-warning:active, -.btn-danger:active, -.btn-default.active, -.btn-primary.active, -.btn-success.active, -.btn-info.active, -.btn-warning.active, -.btn-danger.active { - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); -} -.btn:active, -.btn.active { - background-image: none; -} -.btn-default { - text-shadow: 0 1px 0 #fff; - background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); - background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #dbdbdb; - border-color: #ccc; -} -.btn-default:hover, -.btn-default:focus { - background-color: #e0e0e0; - background-position: 0 -15px; -} -.btn-default:active, -.btn-default.active { - background-color: #e0e0e0; - border-color: #dbdbdb; -} -.btn-primary { - background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #2b669a; -} -.btn-primary:hover, -.btn-primary:focus { - background-color: #2d6ca2; - background-position: 0 -15px; -} -.btn-primary:active, -.btn-primary.active { - background-color: #2d6ca2; - border-color: #2b669a; -} -.btn-success { - background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); - background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #3e8f3e; -} -.btn-success:hover, -.btn-success:focus { - background-color: #419641; - background-position: 0 -15px; -} -.btn-success:active, -.btn-success.active { - background-color: #419641; - border-color: #3e8f3e; -} -.btn-info { - background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); - background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #28a4c9; -} -.btn-info:hover, -.btn-info:focus { - background-color: #2aabd2; - background-position: 0 -15px; -} -.btn-info:active, -.btn-info.active { - background-color: #2aabd2; - border-color: #28a4c9; -} -.btn-warning { - background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); - background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #e38d13; -} -.btn-warning:hover, -.btn-warning:focus { - background-color: #eb9316; - background-position: 0 -15px; -} -.btn-warning:active, -.btn-warning.active { - background-color: #eb9316; - border-color: #e38d13; -} -.btn-danger { - background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); - background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #b92c28; -} -.btn-danger:hover, -.btn-danger:focus { - background-color: #c12e2a; - background-position: 0 -15px; -} -.btn-danger:active, -.btn-danger.active { - background-color: #c12e2a; - border-color: #b92c28; -} -.thumbnail, -.img-thumbnail { - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); - box-shadow: 0 1px 2px rgba(0, 0, 0, .075); -} -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - background-color: #e8e8e8; - background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); - background-repeat: repeat-x; -} -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - background-color: #357ebd; - background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); - background-repeat: repeat-x; -} -.navbar-default { - background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); - background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); -} -.navbar-default .navbar-nav > .active > a { - background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%); - background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0); - background-repeat: repeat-x; - -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); - box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); -} -.navbar-brand, -.navbar-nav > li > a { - text-shadow: 0 1px 0 rgba(255, 255, 255, .25); -} -.navbar-inverse { - background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); - background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; -} -.navbar-inverse .navbar-nav > .active > a { - background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%); - background-image: linear-gradient(to bottom, #222 0%, #282828 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0); - background-repeat: repeat-x; - -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); - box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); -} -.navbar-inverse .navbar-brand, -.navbar-inverse .navbar-nav > li > a { - text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); -} -.navbar-static-top, -.navbar-fixed-top, -.navbar-fixed-bottom { - border-radius: 0; -} -.alert { - text-shadow: 0 1px 0 rgba(255, 255, 255, .2); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); -} -.alert-success { - background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); - background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); - background-repeat: repeat-x; - border-color: #b2dba1; -} -.alert-info { - background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); - background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); - background-repeat: repeat-x; - border-color: #9acfea; -} -.alert-warning { - background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); - background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); - background-repeat: repeat-x; - border-color: #f5e79e; -} -.alert-danger { - background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); - background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); - background-repeat: repeat-x; - border-color: #dca7a7; -} -.progress { - background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); - background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar { - background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-success { - background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); - background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-info { - background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); - background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-warning { - background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); - background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-danger { - background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); - background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); - background-repeat: repeat-x; -} -.list-group { - border-radius: 4px; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); - box-shadow: 0 1px 2px rgba(0, 0, 0, .075); -} -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - text-shadow: 0 -1px 0 #3071a9; - background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); - background-repeat: repeat-x; - border-color: #3278b3; -} -.panel { - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); - box-shadow: 0 1px 2px rgba(0, 0, 0, .05); -} -.panel-default > .panel-heading { - background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); - background-repeat: repeat-x; -} -.panel-primary > .panel-heading { - background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); - background-repeat: repeat-x; -} -.panel-success > .panel-heading { - background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); - background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); - background-repeat: repeat-x; -} -.panel-info > .panel-heading { - background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); - background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); - background-repeat: repeat-x; -} -.panel-warning > .panel-heading { - background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); - background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); - background-repeat: repeat-x; -} -.panel-danger > .panel-heading { - background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); - background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); - background-repeat: repeat-x; -} -.well { - background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); - background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); - background-repeat: repeat-x; - border-color: #dcdcdc; - -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); -} -/*# sourceMappingURL=bootstrap-theme.css.map */ diff --git a/senpy/static/css/bootstrap-theme.css.map b/senpy/static/css/bootstrap-theme.css.map deleted file mode 100644 index b36fc9a..0000000 --- a/senpy/static/css/bootstrap-theme.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["less/theme.less","less/mixins.less"],"names":[],"mappings":"AAeA;AACA;AACA;AACA;AACA;AACA;EACE,wCAAA;ECoGA,2FAAA;EACQ,mFAAA;;ADhGR,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;AACD,YAAC;AAAD,YAAC;AAAD,YAAC;AAAD,SAAC;AAAD,YAAC;AAAD,WAAC;EC8FD,wDAAA;EACQ,gDAAA;;ADnER,IAAC;AACD,IAAC;EACC,sBAAA;;AAKJ;EC4PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;EAyB2C,yBAAA;EAA2B,kBAAA;;AAvBtE,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAeJ;EC2PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAgBJ;EC0PI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAiBJ;ECyPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,SAAC;AACD,SAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,SAAC;AACD,SAAC;EACC,yBAAA;EACA,qBAAA;;AAkBJ;ECwPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,YAAC;AACD,YAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,YAAC;AACD,YAAC;EACC,yBAAA;EACA,qBAAA;;AAmBJ;ECuPI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EAEA,sHAAA;EAoCF,mEAAA;ED7TA,2BAAA;EACA,qBAAA;;AAEA,WAAC;AACD,WAAC;EACC,yBAAA;EACA,4BAAA;;AAGF,WAAC;AACD,WAAC;EACC,yBAAA;EACA,qBAAA;;AA2BJ;AACA;EC6CE,kDAAA;EACQ,0CAAA;;ADpCV,cAAe,KAAK,IAAG;AACvB,cAAe,KAAK,IAAG;ECmOnB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EDpOF,yBAAA;;AAEF,cAAe,UAAU;AACzB,cAAe,UAAU,IAAG;AAC5B,cAAe,UAAU,IAAG;EC6NxB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED9NF,yBAAA;;AAUF;ECiNI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;EDrPA,kBAAA;ECaA,2FAAA;EACQ,mFAAA;;ADjBV,eAOE,YAAY,UAAU;EC0MpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,wDAAA;EACQ,gDAAA;;ADLV;AACA,WAAY,KAAK;EACf,8CAAA;;AAIF;EC+LI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EAoCF,mEAAA;;ADtOF,eAIE,YAAY,UAAU;EC2LpB,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;EApMF,uDAAA;EACQ,+CAAA;;ADCV,eASE;AATF,eAUE,YAAY,KAAK;EACf,yCAAA;;AAKJ;AACA;AACA;EACE,gBAAA;;AAUF;EACE,6CAAA;EChCA,0FAAA;EACQ,kFAAA;;AD2CV;ECqJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAKF;ECoJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAMF;ECmJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAOF;ECkJI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED5JF,qBAAA;;AAgBF;ECyII,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADlIJ;EC+HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADjIJ;EC8HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADhIJ;EC6HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD/HJ;EC4HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD9HJ;EC2HI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtHJ;EACE,kBAAA;EC/EA,kDAAA;EACQ,0CAAA;;ADiFV,gBAAgB;AAChB,gBAAgB,OAAO;AACvB,gBAAgB,OAAO;EACrB,6BAAA;EC4GE,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED7GF,qBAAA;;AAUF;ECjGE,iDAAA;EACQ,yCAAA;;AD0GV,cAAe;ECsFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADxFJ,cAAe;ECqFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADvFJ,cAAe;ECoFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADtFJ,WAAY;ECmFR,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADrFJ,cAAe;ECkFX,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;ADpFJ,aAAc;ECiFV,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;;AD5EJ;ECyEI,kBAAkB,sDAAlB;EACA,kBAAkB,oDAAlB;EACA,2BAAA;EACA,sHAAA;ED1EF,qBAAA;EC1HA,yFAAA;EACQ,iFAAA","sourcesContent":["\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-bg, 5%); @end-color: darken(@navbar-default-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-bg; @end-color: lighten(@navbar-inverse-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n}\n\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","//\n// Mixins\n// --------------------------------------------------\n\n\n// Utilities\n// -------------------------\n\n// Clearfix\n// Source: http://nicolasgallagher.com/micro-clearfix-hack/\n//\n// For modern browsers\n// 1. The space content is one way to avoid an Opera bug when the\n// contenteditable attribute is included anywhere else in the document.\n// Otherwise it causes space to appear at the top and bottom of elements\n// that are clearfixed.\n// 2. The use of `table` rather than `block` is only necessary if using\n// `:before` to contain the top-margins of child elements.\n.clearfix() {\n &:before,\n &:after {\n content: \" \"; // 1\n display: table; // 2\n }\n &:after {\n clear: both;\n }\n}\n\n// WebKit-style focus\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n\n// Center-align a block level element\n.center-block() {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n\n// Sizing shortcuts\n.size(@width; @height) {\n width: @width;\n height: @height;\n}\n.square(@size) {\n .size(@size; @size);\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n &::-moz-placeholder { color: @color; // Firefox\n opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Text overflow\n// Requires inline-block or block for proper styling\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n// CSS image replacement\n//\n// Heads up! v3 launched with with only `.hide-text()`, but per our pattern for\n// mixins being reused as classes with the same name, this doesn't hold up. As\n// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. Note\n// that we cannot chain the mixins together in Less, so they are repeated.\n//\n// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757\n\n// Deprecated as of v3.0.1 (will be removed in v4)\n.hide-text() {\n font: ~\"0/0\" a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n// New mixin to use as of v3.0.1\n.text-hide() {\n .hide-text();\n}\n\n\n\n// CSS3 PROPERTIES\n// --------------------------------------------------\n\n// Single side border-radius\n.border-top-radius(@radius) {\n border-top-right-radius: @radius;\n border-top-left-radius: @radius;\n}\n.border-right-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-top-right-radius: @radius;\n}\n.border-bottom-radius(@radius) {\n border-bottom-right-radius: @radius;\n border-bottom-left-radius: @radius;\n}\n.border-left-radius(@radius) {\n border-bottom-left-radius: @radius;\n border-top-left-radius: @radius;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support the\n// standard `box-shadow` property.\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Transitions\n.transition(@transition) {\n -webkit-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n// Transformations\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n transform: rotate(@degrees);\n}\n.scale(@ratio; @ratio-y...) {\n -webkit-transform: scale(@ratio, @ratio-y);\n -ms-transform: scale(@ratio, @ratio-y); // IE9 only\n transform: scale(@ratio, @ratio-y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n transform: translate(@x, @y);\n}\n.skew(@x; @y) {\n -webkit-transform: skew(@x, @y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n transform: skew(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// User select\n// For selecting text on the page\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n\n// Resize anything\n.resizable(@direction) {\n resize: @direction; // Options: horizontal, vertical, both\n overflow: auto; // Safari fix\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Opacity\n.opacity(@opacity) {\n opacity: @opacity;\n // IE8 filter\n @opacity-ie: (@opacity * 100);\n filter: ~\"alpha(opacity=@{opacity-ie})\";\n}\n\n\n\n// GRADIENTS\n// --------------------------------------------------\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, color-stop(@start-color @start-percent), color-stop(@end-color @end-percent)); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n\n// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n\n\n\n// Retina images\n//\n// Short retina mixin for setting background-image and -size\n\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// COMPONENT MIXINS\n// --------------------------------------------------\n\n// Horizontal dividers\n// -------------------------\n// Dividers (basically an hr) within dropdowns and nav lists\n.nav-divider(@color: #e5e5e5) {\n height: 1px;\n margin: ((@line-height-computed / 2) - 1) 0;\n overflow: hidden;\n background-color: @color;\n}\n\n// Panels\n// -------------------------\n.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) {\n border-color: @border;\n\n & > .panel-heading {\n color: @heading-text-color;\n background-color: @heading-bg-color;\n border-color: @heading-border;\n\n + .panel-collapse .panel-body {\n border-top-color: @border;\n }\n }\n & > .panel-footer {\n + .panel-collapse .panel-body {\n border-bottom-color: @border;\n }\n }\n}\n\n// Alerts\n// -------------------------\n.alert-variant(@background; @border; @text-color) {\n background-color: @background;\n border-color: @border;\n color: @text-color;\n\n hr {\n border-top-color: darken(@border, 5%);\n }\n .alert-link {\n color: darken(@text-color, 10%);\n }\n}\n\n// Tables\n// -------------------------\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n\n// List Groups\n// -------------------------\n.list-group-item-variant(@state; @background; @color) {\n .list-group-item-@{state} {\n color: @color;\n background-color: @background;\n\n a& {\n color: @color;\n\n .list-group-item-heading { color: inherit; }\n\n &:hover,\n &:focus {\n color: @color;\n background-color: darken(@background, 5%);\n }\n &.active,\n &.active:hover,\n &.active:focus {\n color: #fff;\n background-color: @color;\n border-color: @color;\n }\n }\n }\n}\n\n// Button variants\n// -------------------------\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n.button-variant(@color; @background; @border) {\n color: @color;\n background-color: @background;\n border-color: @border;\n\n &:hover,\n &:focus,\n &:active,\n &.active,\n .open .dropdown-toggle& {\n color: @color;\n background-color: darken(@background, 8%);\n border-color: darken(@border, 12%);\n }\n &:active,\n &.active,\n .open .dropdown-toggle& {\n background-image: none;\n }\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &:active,\n &.active {\n background-color: @background;\n border-color: @border;\n }\n }\n\n .badge {\n color: @background;\n background-color: @color;\n }\n}\n\n// Button sizes\n// -------------------------\n.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n}\n\n// Pagination\n// -------------------------\n.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @border-radius) {\n > li {\n > a,\n > span {\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n }\n &:first-child {\n > a,\n > span {\n .border-left-radius(@border-radius);\n }\n }\n &:last-child {\n > a,\n > span {\n .border-right-radius(@border-radius);\n }\n }\n }\n}\n\n// Labels\n// -------------------------\n.label-variant(@color) {\n background-color: @color;\n &[href] {\n &:hover,\n &:focus {\n background-color: darken(@color, 10%);\n }\n }\n}\n\n// Contextual backgrounds\n// -------------------------\n.bg-variant(@color) {\n background-color: @color;\n a&:hover {\n background-color: darken(@color, 10%);\n }\n}\n\n// Typography\n// -------------------------\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover {\n color: darken(@color, 10%);\n }\n}\n\n// Navbar vertical align\n// -------------------------\n// Vertically center elements in the navbar.\n// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.\n.navbar-vertical-align(@element-height) {\n margin-top: ((@navbar-height - @element-height) / 2);\n margin-bottom: ((@navbar-height - @element-height) / 2);\n}\n\n// Progress bars\n// -------------------------\n.progress-bar-variant(@color) {\n background-color: @color;\n .progress-striped & {\n #gradient > .striped();\n }\n}\n\n// Responsive utilities\n// -------------------------\n// More easily include all the states for responsive-utilities.less.\n.responsive-visibility() {\n display: block !important;\n table& { display: table; }\n tr& { display: table-row !important; }\n th&,\n td& { display: table-cell !important; }\n}\n\n.responsive-invisibility() {\n display: none !important;\n}\n\n\n// Grid System\n// -----------\n\n// Centered container element\n.container-fixed() {\n margin-right: auto;\n margin-left: auto;\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: (@gutter / -2);\n margin-right: (@gutter / -2);\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n @media (min-width: @screen-xs-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-xs-column-push(@columns) {\n @media (min-width: @screen-xs-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-xs-column-pull(@columns) {\n @media (min-width: @screen-xs-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n\n// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) when (@index = 1) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n\n// Form validation states\n//\n// Used in forms.less to generate the form validation CSS for warnings, errors,\n// and successes.\n\n.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) {\n // Color the label and help text\n .help-block,\n .control-label,\n .radio,\n .checkbox,\n .radio-inline,\n .checkbox-inline {\n color: @text-color;\n }\n // Set the border and box shadow on specific inputs to match\n .form-control {\n border-color: @border-color;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work\n &:focus {\n border-color: darken(@border-color, 10%);\n @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%);\n .box-shadow(@shadow);\n }\n }\n // Set validation states also for addons\n .input-group-addon {\n color: @text-color;\n border-color: @border-color;\n background-color: @background-color;\n }\n // Optional feedback icon\n .form-control-feedback {\n color: @text-color;\n }\n}\n\n// Form control focus state\n//\n// Generate a customized focus state and for any input with the specified color,\n// which defaults to the `@input-focus-border` variable.\n//\n// We highly encourage you to not customize the default value, but instead use\n// this to tweak colors on an as-needed basis. This aesthetic change is based on\n// WebKit's default styles, but applicable to a wider range of browsers. Its\n// usability and accessibility should be taken into account with any change.\n//\n// Example usage: change the default blue border and shadow to white for better\n// contrast against a dark gray background.\n\n.form-control-focus(@color: @input-border-focus) {\n @color-rgba: rgba(red(@color), green(@color), blue(@color), .6);\n &:focus {\n border-color: @color;\n outline: 0;\n .box-shadow(~\"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}\");\n }\n}\n\n// Form control sizing\n//\n// Relative text size, padding, and border-radii changes for form controls. For\n// horizontal sizing, wrap controls in the predefined grid classes. ``\n// element gets special love because it's special, and that's a fact!\n\n.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) {\n height: @input-height;\n padding: @padding-vertical @padding-horizontal;\n font-size: @font-size;\n line-height: @line-height;\n border-radius: @border-radius;\n\n select& {\n height: @input-height;\n line-height: @input-height;\n }\n\n textarea&,\n select[multiple]& {\n height: auto;\n }\n}\n","//\n// Variables\n// --------------------------------------------------\n\n\n//== Colors\n//\n//## Gray and brand colors for use across Bootstrap.\n\n@gray-darker: lighten(#000, 13.5%); // #222\n@gray-dark: lighten(#000, 20%); // #333\n@gray: lighten(#000, 33.5%); // #555\n@gray-light: lighten(#000, 60%); // #999\n@gray-lighter: lighten(#000, 93.5%); // #eee\n\n@brand-primary: #428bca;\n@brand-success: #5cb85c;\n@brand-info: #5bc0de;\n@brand-warning: #f0ad4e;\n@brand-danger: #d9534f;\n\n\n//== Scaffolding\n//\n// ## Settings for some of the most global styles.\n\n//** Background color for ``.\n@body-bg: #fff;\n//** Global text color on ``.\n@text-color: @gray-dark;\n\n//** Global textual link color.\n@link-color: @brand-primary;\n//** Link hover color set via `darken()` function.\n@link-hover-color: darken(@link-color, 15%);\n\n\n//== Typography\n//\n//## Font, line-height, and color for body text, headings, and more.\n\n@font-family-sans-serif: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n@font-family-serif: Georgia, \"Times New Roman\", Times, serif;\n//** Default monospace fonts for ``, ``, and `
`.\n@font-family-monospace:   Menlo, Monaco, Consolas, \"Courier New\", monospace;\n@font-family-base:        @font-family-sans-serif;\n\n@font-size-base:          14px;\n@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px\n@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px\n\n@font-size-h1:            floor((@font-size-base * 2.6)); // ~36px\n@font-size-h2:            floor((@font-size-base * 2.15)); // ~30px\n@font-size-h3:            ceil((@font-size-base * 1.7)); // ~24px\n@font-size-h4:            ceil((@font-size-base * 1.25)); // ~18px\n@font-size-h5:            @font-size-base;\n@font-size-h6:            ceil((@font-size-base * 0.85)); // ~12px\n\n//** Unit-less `line-height` for use in components like buttons.\n@line-height-base:        1.428571429; // 20/14\n//** Computed \"line-height\" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.\n@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px\n\n//** By default, this inherits from the ``.\n@headings-font-family:    inherit;\n@headings-font-weight:    500;\n@headings-line-height:    1.1;\n@headings-color:          inherit;\n\n\n//-- Iconography\n//\n//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.\n\n@icon-font-path:          \"../fonts/\";\n@icon-font-name:          \"glyphicons-halflings-regular\";\n@icon-font-svg-id:        \"glyphicons_halflingsregular\";\n\n//== Components\n//\n//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).\n\n@padding-base-vertical:     6px;\n@padding-base-horizontal:   12px;\n\n@padding-large-vertical:    10px;\n@padding-large-horizontal:  16px;\n\n@padding-small-vertical:    5px;\n@padding-small-horizontal:  10px;\n\n@padding-xs-vertical:       1px;\n@padding-xs-horizontal:     5px;\n\n@line-height-large:         1.33;\n@line-height-small:         1.5;\n\n@border-radius-base:        4px;\n@border-radius-large:       6px;\n@border-radius-small:       3px;\n\n//** Global color for active items (e.g., navs or dropdowns).\n@component-active-color:    #fff;\n//** Global background color for active items (e.g., navs or dropdowns).\n@component-active-bg:       @brand-primary;\n\n//** Width of the `border` for generating carets that indicator dropdowns.\n@caret-width-base:          4px;\n//** Carets increase slightly in size for larger components.\n@caret-width-large:         5px;\n\n\n//== Tables\n//\n//## Customizes the `.table` component with basic values, each used across all table variations.\n\n//** Padding for ``s and ``s.\n@table-cell-padding:            8px;\n//** Padding for cells in `.table-condensed`.\n@table-condensed-cell-padding:  5px;\n\n//** Default background color used for all tables.\n@table-bg:                      transparent;\n//** Background color used for `.table-striped`.\n@table-bg-accent:               #f9f9f9;\n//** Background color used for `.table-hover`.\n@table-bg-hover:                #f5f5f5;\n@table-bg-active:               @table-bg-hover;\n\n//** Border color for table and cell borders.\n@table-border-color:            #ddd;\n\n\n//== Buttons\n//\n//## For each of Bootstrap's buttons, define text, background and border color.\n\n@btn-font-weight:                normal;\n\n@btn-default-color:              #333;\n@btn-default-bg:                 #fff;\n@btn-default-border:             #ccc;\n\n@btn-primary-color:              #fff;\n@btn-primary-bg:                 @brand-primary;\n@btn-primary-border:             darken(@btn-primary-bg, 5%);\n\n@btn-success-color:              #fff;\n@btn-success-bg:                 @brand-success;\n@btn-success-border:             darken(@btn-success-bg, 5%);\n\n@btn-info-color:                 #fff;\n@btn-info-bg:                    @brand-info;\n@btn-info-border:                darken(@btn-info-bg, 5%);\n\n@btn-warning-color:              #fff;\n@btn-warning-bg:                 @brand-warning;\n@btn-warning-border:             darken(@btn-warning-bg, 5%);\n\n@btn-danger-color:               #fff;\n@btn-danger-bg:                  @brand-danger;\n@btn-danger-border:              darken(@btn-danger-bg, 5%);\n\n@btn-link-disabled-color:        @gray-light;\n\n\n//== Forms\n//\n//##\n\n//** `` background color\n@input-bg:                       #fff;\n//** `` background color\n@input-bg-disabled:              @gray-lighter;\n\n//** Text color for ``s\n@input-color:                    @gray;\n//** `` border color\n@input-border:                   #ccc;\n//** `` border radius\n@input-border-radius:            @border-radius-base;\n//** Border color for inputs on focus\n@input-border-focus:             #66afe9;\n\n//** Placeholder text color\n@input-color-placeholder:        @gray-light;\n\n//** Default `.form-control` height\n@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);\n//** Large `.form-control` height\n@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);\n//** Small `.form-control` height\n@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);\n\n@legend-color:                   @gray-dark;\n@legend-border-color:            #e5e5e5;\n\n//** Background color for textual input addons\n@input-group-addon-bg:           @gray-lighter;\n//** Border color for textual input addons\n@input-group-addon-border-color: @input-border;\n\n\n//== Dropdowns\n//\n//## Dropdown menu container and contents.\n\n//** Background for the dropdown menu.\n@dropdown-bg:                    #fff;\n//** Dropdown menu `border-color`.\n@dropdown-border:                rgba(0,0,0,.15);\n//** Dropdown menu `border-color` **for IE8**.\n@dropdown-fallback-border:       #ccc;\n//** Divider color for between dropdown items.\n@dropdown-divider-bg:            #e5e5e5;\n\n//** Dropdown link text color.\n@dropdown-link-color:            @gray-dark;\n//** Hover color for dropdown links.\n@dropdown-link-hover-color:      darken(@gray-dark, 5%);\n//** Hover background for dropdown links.\n@dropdown-link-hover-bg:         #f5f5f5;\n\n//** Active dropdown menu item text color.\n@dropdown-link-active-color:     @component-active-color;\n//** Active dropdown menu item background color.\n@dropdown-link-active-bg:        @component-active-bg;\n\n//** Disabled dropdown menu item background color.\n@dropdown-link-disabled-color:   @gray-light;\n\n//** Text color for headers within dropdown menus.\n@dropdown-header-color:          @gray-light;\n\n// Note: Deprecated @dropdown-caret-color as of v3.1.0\n@dropdown-caret-color:           #000;\n\n\n//-- Z-index master list\n//\n// Warning: Avoid customizing these values. They're used for a bird's eye view\n// of components dependent on the z-axis and are designed to all work together.\n//\n// Note: These variables are not generated into the Customizer.\n\n@zindex-navbar:            1000;\n@zindex-dropdown:          1000;\n@zindex-popover:           1010;\n@zindex-tooltip:           1030;\n@zindex-navbar-fixed:      1030;\n@zindex-modal-background:  1040;\n@zindex-modal:             1050;\n\n\n//== Media queries breakpoints\n//\n//## Define the breakpoints at which your layout will change, adapting to different screen sizes.\n\n// Extra small screen / phone\n// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1\n@screen-xs:                  480px;\n@screen-xs-min:              @screen-xs;\n@screen-phone:               @screen-xs-min;\n\n// Small screen / tablet\n// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1\n@screen-sm:                  768px;\n@screen-sm-min:              @screen-sm;\n@screen-tablet:              @screen-sm-min;\n\n// Medium screen / desktop\n// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1\n@screen-md:                  992px;\n@screen-md-min:              @screen-md;\n@screen-desktop:             @screen-md-min;\n\n// Large screen / wide desktop\n// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1\n@screen-lg:                  1200px;\n@screen-lg-min:              @screen-lg;\n@screen-lg-desktop:          @screen-lg-min;\n\n// So media queries don't overlap when required, provide a maximum\n@screen-xs-max:              (@screen-sm-min - 1);\n@screen-sm-max:              (@screen-md-min - 1);\n@screen-md-max:              (@screen-lg-min - 1);\n\n\n//== Grid system\n//\n//## Define your custom responsive grid.\n\n//** Number of columns in the grid.\n@grid-columns:              12;\n//** Padding between columns. Gets divided in half for the left and right.\n@grid-gutter-width:         30px;\n// Navbar collapse\n//** Point at which the navbar becomes uncollapsed.\n@grid-float-breakpoint:     @screen-sm-min;\n//** Point at which the navbar begins collapsing.\n@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);\n\n\n//== Container sizes\n//\n//## Define the maximum width of `.container` for different screen sizes.\n\n// Small screen / tablet\n@container-tablet:             ((720px + @grid-gutter-width));\n//** For `@screen-sm-min` and up.\n@container-sm:                 @container-tablet;\n\n// Medium screen / desktop\n@container-desktop:            ((940px + @grid-gutter-width));\n//** For `@screen-md-min` and up.\n@container-md:                 @container-desktop;\n\n// Large screen / wide desktop\n@container-large-desktop:      ((1140px + @grid-gutter-width));\n//** For `@screen-lg-min` and up.\n@container-lg:                 @container-large-desktop;\n\n\n//== Navbar\n//\n//##\n\n// Basics of a navbar\n@navbar-height:                    50px;\n@navbar-margin-bottom:             @line-height-computed;\n@navbar-border-radius:             @border-radius-base;\n@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));\n@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);\n@navbar-collapse-max-height:       340px;\n\n@navbar-default-color:             #777;\n@navbar-default-bg:                #f8f8f8;\n@navbar-default-border:            darken(@navbar-default-bg, 6.5%);\n\n// Navbar links\n@navbar-default-link-color:                #777;\n@navbar-default-link-hover-color:          #333;\n@navbar-default-link-hover-bg:             transparent;\n@navbar-default-link-active-color:         #555;\n@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);\n@navbar-default-link-disabled-color:       #ccc;\n@navbar-default-link-disabled-bg:          transparent;\n\n// Navbar brand label\n@navbar-default-brand-color:               @navbar-default-link-color;\n@navbar-default-brand-hover-color:         darken(@navbar-default-brand-color, 10%);\n@navbar-default-brand-hover-bg:            transparent;\n\n// Navbar toggle\n@navbar-default-toggle-hover-bg:           #ddd;\n@navbar-default-toggle-icon-bar-bg:        #888;\n@navbar-default-toggle-border-color:       #ddd;\n\n\n// Inverted navbar\n// Reset inverted navbar basics\n@navbar-inverse-color:                      @gray-light;\n@navbar-inverse-bg:                         #222;\n@navbar-inverse-border:                     darken(@navbar-inverse-bg, 10%);\n\n// Inverted navbar links\n@navbar-inverse-link-color:                 @gray-light;\n@navbar-inverse-link-hover-color:           #fff;\n@navbar-inverse-link-hover-bg:              transparent;\n@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;\n@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);\n@navbar-inverse-link-disabled-color:        #444;\n@navbar-inverse-link-disabled-bg:           transparent;\n\n// Inverted navbar brand label\n@navbar-inverse-brand-color:                @navbar-inverse-link-color;\n@navbar-inverse-brand-hover-color:          #fff;\n@navbar-inverse-brand-hover-bg:             transparent;\n\n// Inverted navbar toggle\n@navbar-inverse-toggle-hover-bg:            #333;\n@navbar-inverse-toggle-icon-bar-bg:         #fff;\n@navbar-inverse-toggle-border-color:        #333;\n\n\n//== Navs\n//\n//##\n\n//=== Shared nav styles\n@nav-link-padding:                          10px 15px;\n@nav-link-hover-bg:                         @gray-lighter;\n\n@nav-disabled-link-color:                   @gray-light;\n@nav-disabled-link-hover-color:             @gray-light;\n\n@nav-open-link-hover-color:                 #fff;\n\n//== Tabs\n@nav-tabs-border-color:                     #ddd;\n\n@nav-tabs-link-hover-border-color:          @gray-lighter;\n\n@nav-tabs-active-link-hover-bg:             @body-bg;\n@nav-tabs-active-link-hover-color:          @gray;\n@nav-tabs-active-link-hover-border-color:   #ddd;\n\n@nav-tabs-justified-link-border-color:            #ddd;\n@nav-tabs-justified-active-link-border-color:     @body-bg;\n\n//== Pills\n@nav-pills-border-radius:                   @border-radius-base;\n@nav-pills-active-link-hover-bg:            @component-active-bg;\n@nav-pills-active-link-hover-color:         @component-active-color;\n\n\n//== Pagination\n//\n//##\n\n@pagination-color:                     @link-color;\n@pagination-bg:                        #fff;\n@pagination-border:                    #ddd;\n\n@pagination-hover-color:               @link-hover-color;\n@pagination-hover-bg:                  @gray-lighter;\n@pagination-hover-border:              #ddd;\n\n@pagination-active-color:              #fff;\n@pagination-active-bg:                 @brand-primary;\n@pagination-active-border:             @brand-primary;\n\n@pagination-disabled-color:            @gray-light;\n@pagination-disabled-bg:               #fff;\n@pagination-disabled-border:           #ddd;\n\n\n//== Pager\n//\n//##\n\n@pager-bg:                             @pagination-bg;\n@pager-border:                         @pagination-border;\n@pager-border-radius:                  15px;\n\n@pager-hover-bg:                       @pagination-hover-bg;\n\n@pager-active-bg:                      @pagination-active-bg;\n@pager-active-color:                   @pagination-active-color;\n\n@pager-disabled-color:                 @pagination-disabled-color;\n\n\n//== Jumbotron\n//\n//##\n\n@jumbotron-padding:              30px;\n@jumbotron-color:                inherit;\n@jumbotron-bg:                   @gray-lighter;\n@jumbotron-heading-color:        inherit;\n@jumbotron-font-size:            ceil((@font-size-base * 1.5));\n\n\n//== Form states and alerts\n//\n//## Define colors for form feedback states and, by default, alerts.\n\n@state-success-text:             #3c763d;\n@state-success-bg:               #dff0d8;\n@state-success-border:           darken(spin(@state-success-bg, -10), 5%);\n\n@state-info-text:                #31708f;\n@state-info-bg:                  #d9edf7;\n@state-info-border:              darken(spin(@state-info-bg, -10), 7%);\n\n@state-warning-text:             #8a6d3b;\n@state-warning-bg:               #fcf8e3;\n@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);\n\n@state-danger-text:              #a94442;\n@state-danger-bg:                #f2dede;\n@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);\n\n\n//== Tooltips\n//\n//##\n\n//** Tooltip max width\n@tooltip-max-width:           200px;\n//** Tooltip text color\n@tooltip-color:               #fff;\n//** Tooltip background color\n@tooltip-bg:                  #000;\n@tooltip-opacity:             .9;\n\n//** Tooltip arrow width\n@tooltip-arrow-width:         5px;\n//** Tooltip arrow color\n@tooltip-arrow-color:         @tooltip-bg;\n\n\n//== Popovers\n//\n//##\n\n//** Popover body background color\n@popover-bg:                          #fff;\n//** Popover maximum width\n@popover-max-width:                   276px;\n//** Popover border color\n@popover-border-color:                rgba(0,0,0,.2);\n//** Popover fallback border color\n@popover-fallback-border-color:       #ccc;\n\n//** Popover title background color\n@popover-title-bg:                    darken(@popover-bg, 3%);\n\n//** Popover arrow width\n@popover-arrow-width:                 10px;\n//** Popover arrow color\n@popover-arrow-color:                 #fff;\n\n//** Popover outer arrow width\n@popover-arrow-outer-width:           (@popover-arrow-width + 1);\n//** Popover outer arrow color\n@popover-arrow-outer-color:           fadein(@popover-border-color, 5%);\n//** Popover outer arrow fallback color\n@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);\n\n\n//== Labels\n//\n//##\n\n//** Default label background color\n@label-default-bg:            @gray-light;\n//** Primary label background color\n@label-primary-bg:            @brand-primary;\n//** Success label background color\n@label-success-bg:            @brand-success;\n//** Info label background color\n@label-info-bg:               @brand-info;\n//** Warning label background color\n@label-warning-bg:            @brand-warning;\n//** Danger label background color\n@label-danger-bg:             @brand-danger;\n\n//** Default label text color\n@label-color:                 #fff;\n//** Default text color of a linked label\n@label-link-hover-color:      #fff;\n\n\n//== Modals\n//\n//##\n\n//** Padding applied to the modal body\n@modal-inner-padding:         20px;\n\n//** Padding applied to the modal title\n@modal-title-padding:         15px;\n//** Modal title line-height\n@modal-title-line-height:     @line-height-base;\n\n//** Background color of modal content area\n@modal-content-bg:                             #fff;\n//** Modal content border color\n@modal-content-border-color:                   rgba(0,0,0,.2);\n//** Modal content border color **for IE8**\n@modal-content-fallback-border-color:          #999;\n\n//** Modal backdrop background color\n@modal-backdrop-bg:           #000;\n//** Modal backdrop opacity\n@modal-backdrop-opacity:      .5;\n//** Modal header border color\n@modal-header-border-color:   #e5e5e5;\n//** Modal footer border color\n@modal-footer-border-color:   @modal-header-border-color;\n\n@modal-lg:                    900px;\n@modal-md:                    600px;\n@modal-sm:                    300px;\n\n\n//== Alerts\n//\n//## Define alert colors, border radius, and padding.\n\n@alert-padding:               15px;\n@alert-border-radius:         @border-radius-base;\n@alert-link-font-weight:      bold;\n\n@alert-success-bg:            @state-success-bg;\n@alert-success-text:          @state-success-text;\n@alert-success-border:        @state-success-border;\n\n@alert-info-bg:               @state-info-bg;\n@alert-info-text:             @state-info-text;\n@alert-info-border:           @state-info-border;\n\n@alert-warning-bg:            @state-warning-bg;\n@alert-warning-text:          @state-warning-text;\n@alert-warning-border:        @state-warning-border;\n\n@alert-danger-bg:             @state-danger-bg;\n@alert-danger-text:           @state-danger-text;\n@alert-danger-border:         @state-danger-border;\n\n\n//== Progress bars\n//\n//##\n\n//** Background color of the whole progress component\n@progress-bg:                 #f5f5f5;\n//** Progress bar text color\n@progress-bar-color:          #fff;\n\n//** Default progress bar color\n@progress-bar-bg:             @brand-primary;\n//** Success progress bar color\n@progress-bar-success-bg:     @brand-success;\n//** Warning progress bar color\n@progress-bar-warning-bg:     @brand-warning;\n//** Danger progress bar color\n@progress-bar-danger-bg:      @brand-danger;\n//** Info progress bar color\n@progress-bar-info-bg:        @brand-info;\n\n\n//== List group\n//\n//##\n\n//** Background color on `.list-group-item`\n@list-group-bg:                 #fff;\n//** `.list-group-item` border color\n@list-group-border:             #ddd;\n//** List group border radius\n@list-group-border-radius:      @border-radius-base;\n\n//** Background color of single list elements on hover\n@list-group-hover-bg:           #f5f5f5;\n//** Text color of active list elements\n@list-group-active-color:       @component-active-color;\n//** Background color of active list elements\n@list-group-active-bg:          @component-active-bg;\n//** Border color of active list elements\n@list-group-active-border:      @list-group-active-bg;\n@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);\n\n@list-group-link-color:         #555;\n@list-group-link-heading-color: #333;\n\n\n//== Panels\n//\n//##\n\n@panel-bg:                    #fff;\n@panel-body-padding:          15px;\n@panel-border-radius:         @border-radius-base;\n\n//** Border color for elements within panels\n@panel-inner-border:          #ddd;\n@panel-footer-bg:             #f5f5f5;\n\n@panel-default-text:          @gray-dark;\n@panel-default-border:        #ddd;\n@panel-default-heading-bg:    #f5f5f5;\n\n@panel-primary-text:          #fff;\n@panel-primary-border:        @brand-primary;\n@panel-primary-heading-bg:    @brand-primary;\n\n@panel-success-text:          @state-success-text;\n@panel-success-border:        @state-success-border;\n@panel-success-heading-bg:    @state-success-bg;\n\n@panel-info-text:             @state-info-text;\n@panel-info-border:           @state-info-border;\n@panel-info-heading-bg:       @state-info-bg;\n\n@panel-warning-text:          @state-warning-text;\n@panel-warning-border:        @state-warning-border;\n@panel-warning-heading-bg:    @state-warning-bg;\n\n@panel-danger-text:           @state-danger-text;\n@panel-danger-border:         @state-danger-border;\n@panel-danger-heading-bg:     @state-danger-bg;\n\n\n//== Thumbnails\n//\n//##\n\n//** Padding around the thumbnail image\n@thumbnail-padding:           4px;\n//** Thumbnail background color\n@thumbnail-bg:                @body-bg;\n//** Thumbnail border color\n@thumbnail-border:            #ddd;\n//** Thumbnail border radius\n@thumbnail-border-radius:     @border-radius-base;\n\n//** Custom text color for thumbnail captions\n@thumbnail-caption-color:     @text-color;\n//** Padding around the thumbnail caption\n@thumbnail-caption-padding:   9px;\n\n\n//== Wells\n//\n//##\n\n@well-bg:                     #f5f5f5;\n@well-border:                 darken(@well-bg, 7%);\n\n\n//== Badges\n//\n//##\n\n@badge-color:                 #fff;\n//** Linked badge text color on hover\n@badge-link-hover-color:      #fff;\n@badge-bg:                    @gray-light;\n\n//** Badge text color in active nav link\n@badge-active-color:          @link-color;\n//** Badge background color in active nav link\n@badge-active-bg:             #fff;\n\n@badge-font-weight:           bold;\n@badge-line-height:           1;\n@badge-border-radius:         10px;\n\n\n//== Breadcrumbs\n//\n//##\n\n@breadcrumb-padding-vertical:   8px;\n@breadcrumb-padding-horizontal: 15px;\n//** Breadcrumb background color\n@breadcrumb-bg:                 #f5f5f5;\n//** Breadcrumb text color\n@breadcrumb-color:              #ccc;\n//** Text color of current page in the breadcrumb\n@breadcrumb-active-color:       @gray-light;\n//** Textual separator for between breadcrumb elements\n@breadcrumb-separator:          \"/\";\n\n\n//== Carousel\n//\n//##\n\n@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);\n\n@carousel-control-color:                      #fff;\n@carousel-control-width:                      15%;\n@carousel-control-opacity:                    .5;\n@carousel-control-font-size:                  20px;\n\n@carousel-indicator-active-bg:                #fff;\n@carousel-indicator-border-color:             #fff;\n\n@carousel-caption-color:                      #fff;\n\n\n//== Close\n//\n//##\n\n@close-font-weight:           bold;\n@close-color:                 #000;\n@close-text-shadow:           0 1px 0 #fff;\n\n\n//== Code\n//\n//##\n\n@code-color:                  #c7254e;\n@code-bg:                     #f9f2f4;\n\n@kbd-color:                   #fff;\n@kbd-bg:                      #333;\n\n@pre-bg:                      #f5f5f5;\n@pre-color:                   @gray-dark;\n@pre-border-color:            #ccc;\n@pre-scrollable-max-height:   340px;\n\n\n//== Type\n//\n//##\n\n//** Text muted color\n@text-muted:                  @gray-light;\n//** Abbreviations and acronyms border color\n@abbr-border-color:           @gray-light;\n//** Headings small color\n@headings-small-color:        @gray-light;\n//** Blockquote small color\n@blockquote-small-color:      @gray-light;\n//** Blockquote font size\n@blockquote-font-size:        (@font-size-base * 1.25);\n//** Blockquote border color\n@blockquote-border-color:     @gray-lighter;\n//** Page header border color\n@page-header-border-color:    @gray-lighter;\n\n\n//== Miscellaneous\n//\n//##\n\n//** Horizontal line color.\n@hr-border:                   @gray-lighter;\n\n//** Horizontal offset for forms and lists.\n@component-offset-horizontal: 180px;\n","//\n// Thumbnails\n// --------------------------------------------------\n\n\n// Mixin and adjust the regular image class\n.thumbnail {\n  display: block;\n  padding: @thumbnail-padding;\n  margin-bottom: @line-height-computed;\n  line-height: @line-height-base;\n  background-color: @thumbnail-bg;\n  border: 1px solid @thumbnail-border;\n  border-radius: @thumbnail-border-radius;\n  .transition(all .2s ease-in-out);\n\n  > img,\n  a > img {\n    &:extend(.img-responsive);\n    margin-left: auto;\n    margin-right: auto;\n  }\n\n  // Add a hover state for linked versions only\n  a&:hover,\n  a&:focus,\n  a&.active {\n    border-color: @link-color;\n  }\n\n  // Image captions\n  .caption {\n    padding: @thumbnail-caption-padding;\n    color: @thumbnail-caption-color;\n  }\n}\n","//\n// Carousel\n// --------------------------------------------------\n\n\n// Wrapper for the slide container and indicators\n.carousel {\n  position: relative;\n}\n\n.carousel-inner {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n\n  > .item {\n    display: none;\n    position: relative;\n    .transition(.6s ease-in-out left);\n\n    // Account for jankitude on images\n    > img,\n    > a > img {\n      &:extend(.img-responsive);\n      line-height: 1;\n    }\n  }\n\n  > .active,\n  > .next,\n  > .prev { display: block; }\n\n  > .active {\n    left: 0;\n  }\n\n  > .next,\n  > .prev {\n    position: absolute;\n    top: 0;\n    width: 100%;\n  }\n\n  > .next {\n    left: 100%;\n  }\n  > .prev {\n    left: -100%;\n  }\n  > .next.left,\n  > .prev.right {\n    left: 0;\n  }\n\n  > .active.left {\n    left: -100%;\n  }\n  > .active.right {\n    left: 100%;\n  }\n\n}\n\n// Left/right controls for nav\n// ---------------------------\n\n.carousel-control {\n  position: absolute;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  width: @carousel-control-width;\n  .opacity(@carousel-control-opacity);\n  font-size: @carousel-control-font-size;\n  color: @carousel-control-color;\n  text-align: center;\n  text-shadow: @carousel-text-shadow;\n  // We can't have this transition here because WebKit cancels the carousel\n  // animation if you trip this while in the middle of another animation.\n\n  // Set gradients for backgrounds\n  &.left {\n    #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001));\n  }\n  &.right {\n    left: auto;\n    right: 0;\n    #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5));\n  }\n\n  // Hover/focus state\n  &:hover,\n  &:focus {\n    outline: none;\n    color: @carousel-control-color;\n    text-decoration: none;\n    .opacity(.9);\n  }\n\n  // Toggles\n  .icon-prev,\n  .icon-next,\n  .glyphicon-chevron-left,\n  .glyphicon-chevron-right {\n    position: absolute;\n    top: 50%;\n    z-index: 5;\n    display: inline-block;\n  }\n  .icon-prev,\n  .glyphicon-chevron-left {\n    left: 50%;\n  }\n  .icon-next,\n  .glyphicon-chevron-right {\n    right: 50%;\n  }\n  .icon-prev,\n  .icon-next {\n    width:  20px;\n    height: 20px;\n    margin-top: -10px;\n    margin-left: -10px;\n    font-family: serif;\n  }\n\n  .icon-prev {\n    &:before {\n      content: '\\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)\n    }\n  }\n  .icon-next {\n    &:before {\n      content: '\\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)\n    }\n  }\n}\n\n// Optional indicator pips\n//\n// Add an unordered list with the following class and add a list item for each\n// slide your carousel holds.\n\n.carousel-indicators {\n  position: absolute;\n  bottom: 10px;\n  left: 50%;\n  z-index: 15;\n  width: 60%;\n  margin-left: -30%;\n  padding-left: 0;\n  list-style: none;\n  text-align: center;\n\n  li {\n    display: inline-block;\n    width:  10px;\n    height: 10px;\n    margin: 1px;\n    text-indent: -999px;\n    border: 1px solid @carousel-indicator-border-color;\n    border-radius: 10px;\n    cursor: pointer;\n\n    // IE8-9 hack for event handling\n    //\n    // Internet Explorer 8-9 does not support clicks on elements without a set\n    // `background-color`. We cannot use `filter` since that's not viewed as a\n    // background color by the browser. Thus, a hack is needed.\n    //\n    // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we\n    // set alpha transparency for the best results possible.\n    background-color: #000 \\9; // IE8\n    background-color: rgba(0,0,0,0); // IE9\n  }\n  .active {\n    margin: 0;\n    width:  12px;\n    height: 12px;\n    background-color: @carousel-indicator-active-bg;\n  }\n}\n\n// Optional captions\n// -----------------------------\n// Hidden by default for smaller viewports\n.carousel-caption {\n  position: absolute;\n  left: 15%;\n  right: 15%;\n  bottom: 20px;\n  z-index: 10;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  color: @carousel-caption-color;\n  text-align: center;\n  text-shadow: @carousel-text-shadow;\n  & .btn {\n    text-shadow: none; // No shadow for button elements in carousel-caption\n  }\n}\n\n\n// Scale up controls for tablets and up\n@media screen and (min-width: @screen-sm-min) {\n\n  // Scale up the controls a smidge\n  .carousel-control {\n    .glyphicon-chevron-left,\n    .glyphicon-chevron-right,\n    .icon-prev,\n    .icon-next {\n      width: 30px;\n      height: 30px;\n      margin-top: -15px;\n      margin-left: -15px;\n      font-size: 30px;\n    }\n  }\n\n  // Show and left align the captions\n  .carousel-caption {\n    left: 20%;\n    right: 20%;\n    padding-bottom: 30px;\n  }\n\n  // Move up the indicators\n  .carousel-indicators {\n    bottom: 20px;\n  }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n  font-family: @headings-font-family;\n  font-weight: @headings-font-weight;\n  line-height: @headings-line-height;\n  color: @headings-color;\n\n  small,\n  .small {\n    font-weight: normal;\n    line-height: 1;\n    color: @headings-small-color;\n  }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n  margin-top: @line-height-computed;\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 65%;\n  }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n  margin-top: (@line-height-computed / 2);\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 75%;\n  }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n  margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n  margin-bottom: @line-height-computed;\n  font-size: floor((@font-size-base * 1.15));\n  font-weight: 200;\n  line-height: 1.4;\n\n  @media (min-width: @screen-sm-min) {\n    font-size: (@font-size-base * 1.5);\n  }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: 14px base font * 85% = about 12px\nsmall,\n.small  { font-size: 85%; }\n\n// Undo browser default styling\ncite    { font-style: normal; }\n\n// Alignment\n.text-left           { text-align: left; }\n.text-right          { text-align: right; }\n.text-center         { text-align: center; }\n.text-justify        { text-align: justify; }\n\n// Contextual colors\n.text-muted {\n  color: @text-muted;\n}\n.text-primary {\n  .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n  .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n  .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n  .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n  .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n  // Given the contrast here, this is the only class to have its color inverted\n  // automatically.\n  color: #fff;\n  .bg-variant(@brand-primary);\n}\n.bg-success {\n  .bg-variant(@state-success-bg);\n}\n.bg-info {\n  .bg-variant(@state-info-bg);\n}\n.bg-warning {\n  .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n  .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n  padding-bottom: ((@line-height-computed / 2) - 1);\n  margin: (@line-height-computed * 2) 0 @line-height-computed;\n  border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// --------------------------------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n  margin-top: 0;\n  margin-bottom: (@line-height-computed / 2);\n  ul,\n  ol {\n    margin-bottom: 0;\n  }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n  .list-unstyled();\n  margin-left: -5px;\n\n  > li {\n    display: inline-block;\n    padding-left: 5px;\n    padding-right: 5px;\n  }\n}\n\n// Description Lists\ndl {\n  margin-top: 0; // Remove browser default\n  margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n  line-height: @line-height-base;\n}\ndt {\n  font-weight: bold;\n}\ndd {\n  margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n@media (min-width: @grid-float-breakpoint) {\n  .dl-horizontal {\n    dt {\n      float: left;\n      width: (@component-offset-horizontal - 20);\n      clear: left;\n      text-align: right;\n      .text-overflow();\n    }\n    dd {\n      margin-left: @component-offset-horizontal;\n      &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n    }\n  }\n}\n\n// MISC\n// ----\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n  cursor: help;\n  border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n  font-size: 90%;\n  text-transform: uppercase;\n}\n\n// Blockquotes\nblockquote {\n  padding: (@line-height-computed / 2) @line-height-computed;\n  margin: 0 0 @line-height-computed;\n  font-size: @blockquote-font-size;\n  border-left: 5px solid @blockquote-border-color;\n\n  p,\n  ul,\n  ol {\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  // Note: Deprecated small and .small as of v3.1.0\n  // Context: https://github.com/twbs/bootstrap/issues/11660\n  footer,\n  small,\n  .small {\n    display: block;\n    font-size: 80%; // back to default font-size\n    line-height: @line-height-base;\n    color: @blockquote-small-color;\n\n    &:before {\n      content: '\\2014 \\00A0'; // em dash, nbsp\n    }\n  }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n  padding-right: 15px;\n  padding-left: 0;\n  border-right: 5px solid @blockquote-border-color;\n  border-left: 0;\n  text-align: right;\n\n  // Account for citation\n  footer,\n  small,\n  .small {\n    &:before { content: ''; }\n    &:after {\n      content: '\\00A0 \\2014'; // nbsp, em dash\n    }\n  }\n}\n\n// Quotes\nblockquote:before,\nblockquote:after {\n  content: \"\";\n}\n\n// Addresses\naddress {\n  margin-bottom: @line-height-computed;\n  font-style: normal;\n  line-height: @line-height-base;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n  font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: @code-color;\n  background-color: @code-bg;\n  white-space: nowrap;\n  border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: @kbd-color;\n  background-color: @kbd-bg;\n  border-radius: @border-radius-small;\n  box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n}\n\n// Blocks of code\npre {\n  display: block;\n  padding: ((@line-height-computed - 1) / 2);\n  margin: 0 0 (@line-height-computed / 2);\n  font-size: (@font-size-base - 1); // 14px to 13px\n  line-height: @line-height-base;\n  word-break: break-all;\n  word-wrap: break-word;\n  color: @pre-color;\n  background-color: @pre-bg;\n  border: 1px solid @pre-border-color;\n  border-radius: @border-radius-base;\n\n  // Account for some code outputs that place code tags in pre tags\n  code {\n    padding: 0;\n    font-size: inherit;\n    color: inherit;\n    white-space: pre-wrap;\n    background-color: transparent;\n    border-radius: 0;\n  }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n  max-height: @pre-scrollable-max-height;\n  overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n  .container-fixed();\n\n  @media (min-width: @screen-sm-min) {\n    width: @container-sm;\n  }\n  @media (min-width: @screen-md-min) {\n    width: @container-md;\n  }\n  @media (min-width: @screen-lg-min) {\n    width: @container-lg;\n  }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n  .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n  .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n  .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n  .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n  .make-grid(lg);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n  max-width: 100%;\n  background-color: @table-bg;\n}\nth {\n  text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n  width: 100%;\n  margin-bottom: @line-height-computed;\n  // Cells\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        padding: @table-cell-padding;\n        line-height: @line-height-base;\n        vertical-align: top;\n        border-top: 1px solid @table-border-color;\n      }\n    }\n  }\n  // Bottom align for column headings\n  > thead > tr > th {\n    vertical-align: bottom;\n    border-bottom: 2px solid @table-border-color;\n  }\n  // Remove top border from thead by default\n  > caption + thead,\n  > colgroup + thead,\n  > thead:first-child {\n    > tr:first-child {\n      > th,\n      > td {\n        border-top: 0;\n      }\n    }\n  }\n  // Account for multiple tbody instances\n  > tbody + tbody {\n    border-top: 2px solid @table-border-color;\n  }\n\n  // Nesting\n  .table {\n    background-color: @body-bg;\n  }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        padding: @table-condensed-cell-padding;\n      }\n    }\n  }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n  border: 1px solid @table-border-color;\n  > thead,\n  > tbody,\n  > tfoot {\n    > tr {\n      > th,\n      > td {\n        border: 1px solid @table-border-color;\n      }\n    }\n  }\n  > thead > tr {\n    > th,\n    > td {\n      border-bottom-width: 2px;\n    }\n  }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n  > tbody > tr:nth-child(odd) {\n    > td,\n    > th {\n      background-color: @table-bg-accent;\n    }\n  }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n  > tbody > tr:hover {\n    > td,\n    > th {\n      background-color: @table-bg-hover;\n    }\n  }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n  position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n  float: none;\n  display: table-column;\n}\ntable {\n  td,\n  th {\n    &[class*=\"col-\"] {\n      position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n      float: none;\n      display: table-cell;\n    }\n  }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n@media (max-width: @screen-xs-max) {\n  .table-responsive {\n    width: 100%;\n    margin-bottom: (@line-height-computed * 0.75);\n    overflow-y: hidden;\n    overflow-x: scroll;\n    -ms-overflow-style: -ms-autohiding-scrollbar;\n    border: 1px solid @table-border-color;\n    -webkit-overflow-scrolling: touch;\n\n    // Tighten up spacing\n    > .table {\n      margin-bottom: 0;\n\n      // Ensure the content doesn't wrap\n      > thead,\n      > tbody,\n      > tfoot {\n        > tr {\n          > th,\n          > td {\n            white-space: nowrap;\n          }\n        }\n      }\n    }\n\n    // Special overrides for the bordered tables\n    > .table-bordered {\n      border: 0;\n\n      // Nuke the appropriate borders so that the parent can handle them\n      > thead,\n      > tbody,\n      > tfoot {\n        > tr {\n          > th:first-child,\n          > td:first-child {\n            border-left: 0;\n          }\n          > th:last-child,\n          > td:last-child {\n            border-right: 0;\n          }\n        }\n      }\n\n      // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n      // chances are there will be only one `tr` in a `thead` and that would\n      // remove the border altogether.\n      > tbody,\n      > tfoot {\n        > tr:last-child {\n          > th,\n          > td {\n            border-bottom: 0;\n          }\n        }\n      }\n\n    }\n  }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n  padding: 0;\n  margin: 0;\n  border: 0;\n  // Chrome and Firefox set a `min-width: -webkit-min-content;` on fieldsets,\n  // so we reset that to ensure it behaves more like a standard block element.\n  // See https://github.com/twbs/bootstrap/issues/12359.\n  min-width: 0;\n}\n\nlegend {\n  display: block;\n  width: 100%;\n  padding: 0;\n  margin-bottom: @line-height-computed;\n  font-size: (@font-size-base * 1.5);\n  line-height: inherit;\n  color: @legend-color;\n  border: 0;\n  border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n  display: inline-block;\n  margin-bottom: 5px;\n  font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n  .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n  margin: 4px 0 0;\n  margin-top: 1px \\9; /* IE8-9 */\n  line-height: normal;\n}\n\n// Set the height of file controls to match text inputs\ninput[type=\"file\"] {\n  display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n  display: block;\n  width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n  height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n  .tab-focus();\n}\n\n// Adjust output element\noutput {\n  display: block;\n  padding-top: (@padding-base-vertical + 1);\n  font-size: @font-size-base;\n  line-height: @line-height-base;\n  color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n  display: block;\n  width: 100%;\n  height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n  padding: @padding-base-vertical @padding-base-horizontal;\n  font-size: @font-size-base;\n  line-height: @line-height-base;\n  color: @input-color;\n  background-color: @input-bg;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid @input-border;\n  border-radius: @input-border-radius;\n  .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n  .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n  // Customize the `:focus` state to imitate native WebKit styles.\n  .form-control-focus();\n\n  // Placeholder\n  .placeholder();\n\n  // Disabled and read-only inputs\n  //\n  // HTML5 says that controls under a fieldset > legend:first-child won't be\n  // disabled if the fieldset is disabled. Due to implementation difficulty, we\n  // don't honor that edge case; we style them as disabled anyway.\n  &[disabled],\n  &[readonly],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n    background-color: @input-bg-disabled;\n    opacity: 1; // iOS fix for unreadable disabled content\n  }\n\n  // Reset height for `textarea`s\n  textarea& {\n    height: auto;\n  }\n}\n\n\n// Search inputs in iOS\n//\n// This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\ninput[type=\"search\"] {\n  -webkit-appearance: none;\n}\n\n\n// Special styles for iOS date input\n//\n// In Mobile Safari, date inputs require a pixel line-height that matches the\n// given height of the input.\n\ninput[type=\"date\"] {\n  line-height: @input-height-base;\n}\n\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n  margin-bottom: 15px;\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.radio,\n.checkbox {\n  display: block;\n  min-height: @line-height-computed; // clear the floating input if there is no label text\n  margin-top: 10px;\n  margin-bottom: 10px;\n  padding-left: 20px;\n  label {\n    display: inline;\n    font-weight: normal;\n    cursor: pointer;\n  }\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n  float: left;\n  margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n  margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing\n}\n\n// Radios and checkboxes on same line\n.radio-inline,\n.checkbox-inline {\n  display: inline-block;\n  padding-left: 20px;\n  margin-bottom: 0;\n  vertical-align: middle;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n  margin-top: 0;\n  margin-left: 10px; // space out consecutive inline controls\n}\n\n// Apply same disabled cursor tweak as for inputs\n//\n// Note: Neither radios nor checkboxes can be readonly.\ninput[type=\"radio\"],\ninput[type=\"checkbox\"],\n.radio,\n.radio-inline,\n.checkbox,\n.checkbox-inline {\n  &[disabled],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n  }\n}\n\n\n// Form control sizing\n//\n// Build on `.form-control` with modifier classes to decrease or increase the\n// height and font-size of form controls.\n\n.input-sm {\n  .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n\n.input-lg {\n  .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n\n\n// Form control feedback states\n//\n// Apply contextual and semantic states to individual form controls.\n\n.has-feedback {\n  // Enable absolute positioning\n  position: relative;\n\n  // Ensure icons don't overlap text\n  .form-control {\n    padding-right: (@input-height-base * 1.25);\n  }\n\n  // Feedback icon (requires .glyphicon classes)\n  .form-control-feedback {\n    position: absolute;\n    top: (@line-height-computed + 5); // Height of the `label` and its margin\n    right: 0;\n    display: block;\n    width: @input-height-base;\n    height: @input-height-base;\n    line-height: @input-height-base;\n    text-align: center;\n  }\n}\n\n// Feedback states\n.has-success {\n  .form-control-validation(@state-success-text; @state-success-text; @state-success-bg);\n}\n.has-warning {\n  .form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg);\n}\n.has-error {\n  .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg);\n}\n\n\n// Static form control text\n//\n// Apply class to a `p` element to make any string of text align with labels in\n// a horizontal form layout.\n\n.form-control-static {\n  margin-bottom: 0; // Remove default margin from `p`\n}\n\n\n// Help text\n//\n// Apply to any element you wish to create light text for placement immediately\n// below a form control. Use for general help, formatting, or instructional text.\n\n.help-block {\n  display: block; // account for any element using help-block\n  margin-top: 5px;\n  margin-bottom: 10px;\n  color: lighten(@text-color, 25%); // lighten the text some for contrast\n}\n\n\n\n// Inline forms\n//\n// Make forms appear inline(-block) by adding the `.form-inline` class. Inline\n// forms begin stacked on extra small (mobile) devices and then go inline when\n// viewports reach <768px.\n//\n// Requires wrapping inputs and labels with `.form-group` for proper display of\n// default HTML form controls and our custom form controls (e.g., input groups).\n//\n// Heads up! This is mixin-ed into `.navbar-form` in navbars.less.\n\n.form-inline {\n\n  // Kick in the inline\n  @media (min-width: @screen-sm-min) {\n    // Inline-block all the things for \"inline\"\n    .form-group {\n      display: inline-block;\n      margin-bottom: 0;\n      vertical-align: middle;\n    }\n\n    // In navbar-form, allow folks to *not* use `.form-group`\n    .form-control {\n      display: inline-block;\n      width: auto; // Prevent labels from stacking above inputs in `.form-group`\n      vertical-align: middle;\n    }\n    // Input groups need that 100% width though\n    .input-group > .form-control {\n      width: 100%;\n    }\n\n    .control-label {\n      margin-bottom: 0;\n      vertical-align: middle;\n    }\n\n    // Remove default margin on radios/checkboxes that were used for stacking, and\n    // then undo the floating of radios and checkboxes to match (which also avoids\n    // a bug in WebKit: https://github.com/twbs/bootstrap/issues/1969).\n    .radio,\n    .checkbox {\n      display: inline-block;\n      margin-top: 0;\n      margin-bottom: 0;\n      padding-left: 0;\n      vertical-align: middle;\n    }\n    .radio input[type=\"radio\"],\n    .checkbox input[type=\"checkbox\"] {\n      float: none;\n      margin-left: 0;\n    }\n\n    // Validation states\n    //\n    // Reposition the icon because it's now within a grid column and columns have\n    // `position: relative;` on them. Also accounts for the grid gutter padding.\n    .has-feedback .form-control-feedback {\n      top: 0;\n    }\n  }\n}\n\n\n// Horizontal forms\n//\n// Horizontal forms are built on grid classes and allow you to create forms with\n// labels on the left and inputs on the right.\n\n.form-horizontal {\n\n  // Consistent vertical alignment of labels, radios, and checkboxes\n  .control-label,\n  .radio,\n  .checkbox,\n  .radio-inline,\n  .checkbox-inline {\n    margin-top: 0;\n    margin-bottom: 0;\n    padding-top: (@padding-base-vertical + 1); // Default padding plus a border\n  }\n  // Account for padding we're adding to ensure the alignment and of help text\n  // and other content below items\n  .radio,\n  .checkbox {\n    min-height: (@line-height-computed + (@padding-base-vertical + 1));\n  }\n\n  // Make form groups behave like rows\n  .form-group {\n    .make-row();\n  }\n\n  .form-control-static {\n    padding-top: (@padding-base-vertical + 1);\n  }\n\n  // Only right align form labels here when the columns stop stacking\n  @media (min-width: @screen-sm-min) {\n    .control-label {\n      text-align: right;\n    }\n  }\n\n  // Validation states\n  //\n  // Reposition the icon because it's now within a grid column and columns have\n  // `position: relative;` on them. Also accounts for the grid gutter padding.\n  .has-feedback .form-control-feedback {\n    top: 0;\n    right: (@grid-gutter-width / 2);\n  }\n}\n","//\n// Buttons\n// --------------------------------------------------\n\n\n// Base styles\n// --------------------------------------------------\n\n.btn {\n  display: inline-block;\n  margin-bottom: 0; // For input.btn\n  font-weight: @btn-font-weight;\n  text-align: center;\n  vertical-align: middle;\n  cursor: pointer;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid transparent;\n  white-space: nowrap;\n  .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base);\n  .user-select(none);\n\n  &,\n  &:active,\n  &.active {\n    &:focus {\n      .tab-focus();\n    }\n  }\n\n  &:hover,\n  &:focus {\n    color: @btn-default-color;\n    text-decoration: none;\n  }\n\n  &:active,\n  &.active {\n    outline: 0;\n    background-image: none;\n    .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n  }\n\n  &.disabled,\n  &[disabled],\n  fieldset[disabled] & {\n    cursor: not-allowed;\n    pointer-events: none; // Future-proof disabling of clicks\n    .opacity(.65);\n    .box-shadow(none);\n  }\n}\n\n\n// Alternate buttons\n// --------------------------------------------------\n\n.btn-default {\n  .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border);\n}\n.btn-primary {\n  .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border);\n}\n// Success appears as green\n.btn-success {\n  .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border);\n}\n// Info appears as blue-green\n.btn-info {\n  .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border);\n}\n// Warning appears as orange\n.btn-warning {\n  .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border);\n}\n// Danger and error appear as red\n.btn-danger {\n  .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border);\n}\n\n\n// Link buttons\n// -------------------------\n\n// Make a button look and behave like a link\n.btn-link {\n  color: @link-color;\n  font-weight: normal;\n  cursor: pointer;\n  border-radius: 0;\n\n  &,\n  &:active,\n  &[disabled],\n  fieldset[disabled] & {\n    background-color: transparent;\n    .box-shadow(none);\n  }\n  &,\n  &:hover,\n  &:focus,\n  &:active {\n    border-color: transparent;\n  }\n  &:hover,\n  &:focus {\n    color: @link-hover-color;\n    text-decoration: underline;\n    background-color: transparent;\n  }\n  &[disabled],\n  fieldset[disabled] & {\n    &:hover,\n    &:focus {\n      color: @btn-link-disabled-color;\n      text-decoration: none;\n    }\n  }\n}\n\n\n// Button Sizes\n// --------------------------------------------------\n\n.btn-lg {\n  // line-height: ensure even-numbered height of button next to large input\n  .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);\n}\n.btn-sm {\n  // line-height: ensure proper height of button next to small input\n  .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n.btn-xs {\n  .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small);\n}\n\n\n// Block button\n// --------------------------------------------------\n\n.btn-block {\n  display: block;\n  width: 100%;\n  padding-left: 0;\n  padding-right: 0;\n}\n\n// Vertically space out multiple block buttons\n.btn-block + .btn-block {\n  margin-top: 5px;\n}\n\n// Specificity overrides\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n  &.btn-block {\n    width: 100%;\n  }\n}\n","//\n// Button groups\n// --------------------------------------------------\n\n// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-block;\n  vertical-align: middle; // match .btn alignment given font-size hack above\n  > .btn {\n    position: relative;\n    float: left;\n    // Bring the \"active\" button to the front\n    &:hover,\n    &:focus,\n    &:active,\n    &.active {\n      z-index: 2;\n    }\n    &:focus {\n      // Remove focus outline when dropdown JS adds it after closing the menu\n      outline: none;\n    }\n  }\n}\n\n// Prevent double borders when buttons are next to each other\n.btn-group {\n  .btn + .btn,\n  .btn + .btn-group,\n  .btn-group + .btn,\n  .btn-group + .btn-group {\n    margin-left: -1px;\n  }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n  margin-left: -5px; // Offset the first child's margin\n  &:extend(.clearfix all);\n\n  .btn-group,\n  .input-group {\n    float: left;\n  }\n  > .btn,\n  > .btn-group,\n  > .input-group {\n    margin-left: 5px;\n  }\n}\n\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n  border-radius: 0;\n}\n\n// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match\n.btn-group > .btn:first-child {\n  margin-left: 0;\n  &:not(:last-child):not(.dropdown-toggle) {\n    .border-right-radius(0);\n  }\n}\n// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n  .border-left-radius(0);\n}\n\n// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)\n.btn-group > .btn-group {\n  float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group > .btn-group:first-child {\n  > .btn:last-child,\n  > .dropdown-toggle {\n    .border-right-radius(0);\n  }\n}\n.btn-group > .btn-group:last-child > .btn:first-child {\n  .border-left-radius(0);\n}\n\n// On active and open, don't show outline\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n  outline: 0;\n}\n\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-xs > .btn { &:extend(.btn-xs); }\n.btn-group-sm > .btn { &:extend(.btn-sm); }\n.btn-group-lg > .btn { &:extend(.btn-lg); }\n\n\n// Split button dropdowns\n// ----------------------\n\n// Give the line between buttons some depth\n.btn-group > .btn + .dropdown-toggle {\n  padding-left: 8px;\n  padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n  padding-left: 12px;\n  padding-right: 12px;\n}\n\n// The clickable button for toggling the menu\n// Remove the gradient and set the same inset shadow as the :active state\n.btn-group.open .dropdown-toggle {\n  .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n\n  // Show no shadow for `.btn-link` since it has no other button styles.\n  &.btn-link {\n    .box-shadow(none);\n  }\n}\n\n\n// Reposition the caret\n.btn .caret {\n  margin-left: 0;\n}\n// Carets in other button sizes\n.btn-lg .caret {\n  border-width: @caret-width-large @caret-width-large 0;\n  border-bottom-width: 0;\n}\n// Upside down carets for .dropup\n.dropup .btn-lg .caret {\n  border-width: 0 @caret-width-large @caret-width-large;\n}\n\n\n// Vertical button groups\n// ----------------------\n\n.btn-group-vertical {\n  > .btn,\n  > .btn-group,\n  > .btn-group > .btn {\n    display: block;\n    float: none;\n    width: 100%;\n    max-width: 100%;\n  }\n\n  // Clear floats so dropdown menus can be properly placed\n  > .btn-group {\n    &:extend(.clearfix all);\n    > .btn {\n      float: none;\n    }\n  }\n\n  > .btn + .btn,\n  > .btn + .btn-group,\n  > .btn-group + .btn,\n  > .btn-group + .btn-group {\n    margin-top: -1px;\n    margin-left: 0;\n  }\n}\n\n.btn-group-vertical > .btn {\n  &:not(:first-child):not(:last-child) {\n    border-radius: 0;\n  }\n  &:first-child:not(:last-child) {\n    border-top-right-radius: @border-radius-base;\n    .border-bottom-radius(0);\n  }\n  &:last-child:not(:first-child) {\n    border-bottom-left-radius: @border-radius-base;\n    .border-top-radius(0);\n  }\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) {\n  > .btn:last-child,\n  > .dropdown-toggle {\n    .border-bottom-radius(0);\n  }\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  .border-top-radius(0);\n}\n\n\n\n// Justified button groups\n// ----------------------\n\n.btn-group-justified {\n  display: table;\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: separate;\n  > .btn,\n  > .btn-group {\n    float: none;\n    display: table-cell;\n    width: 1%;\n  }\n  > .btn-group .btn {\n    width: 100%;\n  }\n}\n\n\n// Checkbox and radio options\n[data-toggle=\"buttons\"] > .btn > input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn > input[type=\"checkbox\"] {\n  display: none;\n}\n","//\n// Component animations\n// --------------------------------------------------\n\n// Heads up!\n//\n// We don't use the `.opacity()` mixin here since it causes a bug with text\n// fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552.\n\n.fade {\n  opacity: 0;\n  .transition(opacity .15s linear);\n  &.in {\n    opacity: 1;\n  }\n}\n\n.collapse {\n  display: none;\n  &.in {\n    display: block;\n  }\n}\n.collapsing {\n  position: relative;\n  height: 0;\n  overflow: hidden;\n  .transition(height .35s ease);\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n//  Star\n\n// Import the fonts\n@font-face {\n  font-family: 'Glyphicons Halflings';\n  src: ~\"url('@{icon-font-path}@{icon-font-name}.eot')\";\n  src: ~\"url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.woff') format('woff')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype')\",\n       ~\"url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg')\";\n}\n\n// Catchall baseclass\n.glyphicon {\n  position: relative;\n  top: 1px;\n  display: inline-block;\n  font-family: 'Glyphicons Halflings';\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk               { &:before { content: \"\\2a\"; } }\n.glyphicon-plus                   { &:before { content: \"\\2b\"; } }\n.glyphicon-euro                   { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus                  { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud                  { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope               { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil                 { &:before { content: \"\\270f\"; } }\n.glyphicon-glass                  { &:before { content: \"\\e001\"; } }\n.glyphicon-music                  { &:before { content: \"\\e002\"; } }\n.glyphicon-search                 { &:before { content: \"\\e003\"; } }\n.glyphicon-heart                  { &:before { content: \"\\e005\"; } }\n.glyphicon-star                   { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty             { &:before { content: \"\\e007\"; } }\n.glyphicon-user                   { &:before { content: \"\\e008\"; } }\n.glyphicon-film                   { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large               { &:before { content: \"\\e010\"; } }\n.glyphicon-th                     { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list                { &:before { content: \"\\e012\"; } }\n.glyphicon-ok                     { &:before { content: \"\\e013\"; } }\n.glyphicon-remove                 { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in                { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out               { &:before { content: \"\\e016\"; } }\n.glyphicon-off                    { &:before { content: \"\\e017\"; } }\n.glyphicon-signal                 { &:before { content: \"\\e018\"; } }\n.glyphicon-cog                    { &:before { content: \"\\e019\"; } }\n.glyphicon-trash                  { &:before { content: \"\\e020\"; } }\n.glyphicon-home                   { &:before { content: \"\\e021\"; } }\n.glyphicon-file                   { &:before { content: \"\\e022\"; } }\n.glyphicon-time                   { &:before { content: \"\\e023\"; } }\n.glyphicon-road                   { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt           { &:before { content: \"\\e025\"; } }\n.glyphicon-download               { &:before { content: \"\\e026\"; } }\n.glyphicon-upload                 { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox                  { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle            { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat                 { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh                { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt               { &:before { content: \"\\e032\"; } }\n.glyphicon-lock                   { &:before { content: \"\\e033\"; } }\n.glyphicon-flag                   { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones             { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off             { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down            { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up              { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode                 { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode                { &:before { content: \"\\e040\"; } }\n.glyphicon-tag                    { &:before { content: \"\\e041\"; } }\n.glyphicon-tags                   { &:before { content: \"\\e042\"; } }\n.glyphicon-book                   { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark               { &:before { content: \"\\e044\"; } }\n.glyphicon-print                  { &:before { content: \"\\e045\"; } }\n.glyphicon-camera                 { &:before { content: \"\\e046\"; } }\n.glyphicon-font                   { &:before { content: \"\\e047\"; } }\n.glyphicon-bold                   { &:before { content: \"\\e048\"; } }\n.glyphicon-italic                 { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height            { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width             { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left             { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center           { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right            { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify          { &:before { content: \"\\e055\"; } }\n.glyphicon-list                   { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left            { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right           { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video         { &:before { content: \"\\e059\"; } }\n.glyphicon-picture                { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker             { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust                 { &:before { content: \"\\e063\"; } }\n.glyphicon-tint                   { &:before { content: \"\\e064\"; } }\n.glyphicon-edit                   { &:before { content: \"\\e065\"; } }\n.glyphicon-share                  { &:before { content: \"\\e066\"; } }\n.glyphicon-check                  { &:before { content: \"\\e067\"; } }\n.glyphicon-move                   { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward          { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward          { &:before { content: \"\\e070\"; } }\n.glyphicon-backward               { &:before { content: \"\\e071\"; } }\n.glyphicon-play                   { &:before { content: \"\\e072\"; } }\n.glyphicon-pause                  { &:before { content: \"\\e073\"; } }\n.glyphicon-stop                   { &:before { content: \"\\e074\"; } }\n.glyphicon-forward                { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward           { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward           { &:before { content: \"\\e077\"; } }\n.glyphicon-eject                  { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left           { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right          { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign              { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign             { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign            { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign                { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign          { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign              { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot             { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle          { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle              { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle             { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left             { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right            { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up               { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down             { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt              { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full            { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small           { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign       { &:before { content: \"\\e101\"; } }\n.glyphicon-gift                   { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf                   { &:before { content: \"\\e103\"; } }\n.glyphicon-fire                   { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open               { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close              { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign           { &:before { content: \"\\e107\"; } }\n.glyphicon-plane                  { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar               { &:before { content: \"\\e109\"; } }\n.glyphicon-random                 { &:before { content: \"\\e110\"; } }\n.glyphicon-comment                { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet                 { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up             { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down           { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet                { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart          { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close           { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open            { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical        { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal      { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd                    { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn               { &:before { content: \"\\e122\"; } }\n.glyphicon-bell                   { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate            { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up              { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down            { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right             { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left              { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up                { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down              { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right     { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left      { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up        { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down      { &:before { content: \"\\e134\"; } }\n.glyphicon-globe                  { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench                 { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks                  { &:before { content: \"\\e137\"; } }\n.glyphicon-filter                 { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase              { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen             { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard              { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip              { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty            { &:before { content: \"\\e143\"; } }\n.glyphicon-link                   { &:before { content: \"\\e144\"; } }\n.glyphicon-phone                  { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin                { &:before { content: \"\\e146\"; } }\n.glyphicon-usd                    { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp                    { &:before { content: \"\\e149\"; } }\n.glyphicon-sort                   { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet       { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt   { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order          { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt      { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes     { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked              { &:before { content: \"\\e157\"; } }\n.glyphicon-expand                 { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down          { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up            { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in                 { &:before { content: \"\\e161\"; } }\n.glyphicon-flash                  { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out                { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window             { &:before { content: \"\\e164\"; } }\n.glyphicon-record                 { &:before { content: \"\\e165\"; } }\n.glyphicon-save                   { &:before { content: \"\\e166\"; } }\n.glyphicon-open                   { &:before { content: \"\\e167\"; } }\n.glyphicon-saved                  { &:before { content: \"\\e168\"; } }\n.glyphicon-import                 { &:before { content: \"\\e169\"; } }\n.glyphicon-export                 { &:before { content: \"\\e170\"; } }\n.glyphicon-send                   { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk            { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved           { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove          { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save            { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open            { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card            { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer               { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery                { &:before { content: \"\\e179\"; } }\n.glyphicon-header                 { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed             { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone               { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt              { &:before { content: \"\\e183\"; } }\n.glyphicon-tower                  { &:before { content: \"\\e184\"; } }\n.glyphicon-stats                  { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video               { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video               { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles              { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo           { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby            { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1              { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1              { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1              { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark         { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark      { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download         { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload           { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer           { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous         { &:before { content: \"\\e200\"; } }\n","//\n// Dropdown menus\n// --------------------------------------------------\n\n\n// Dropdown arrow/caret\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top:   @caret-width-base solid;\n  border-right: @caret-width-base solid transparent;\n  border-left:  @caret-width-base solid transparent;\n}\n\n// The dropdown wrapper (div)\n.dropdown {\n  position: relative;\n}\n\n// Prevent the focus on the dropdown toggle when closing dropdowns\n.dropdown-toggle:focus {\n  outline: 0;\n}\n\n// The dropdown menu (ul)\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: @zindex-dropdown;\n  display: none; // none by default, but block on \"open\" of the menu\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0; // override default ul\n  list-style: none;\n  font-size: @font-size-base;\n  background-color: @dropdown-bg;\n  border: 1px solid @dropdown-fallback-border; // IE8 fallback\n  border: 1px solid @dropdown-border;\n  border-radius: @border-radius-base;\n  .box-shadow(0 6px 12px rgba(0,0,0,.175));\n  background-clip: padding-box;\n\n  // Aligns the dropdown menu to right\n  //\n  // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`\n  &.pull-right {\n    right: 0;\n    left: auto;\n  }\n\n  // Dividers (basically an hr) within the dropdown\n  .divider {\n    .nav-divider(@dropdown-divider-bg);\n  }\n\n  // Links within the dropdown menu\n  > li > a {\n    display: block;\n    padding: 3px 20px;\n    clear: both;\n    font-weight: normal;\n    line-height: @line-height-base;\n    color: @dropdown-link-color;\n    white-space: nowrap; // prevent links from randomly breaking onto new lines\n  }\n}\n\n// Hover/Focus state\n.dropdown-menu > li > a {\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    color: @dropdown-link-hover-color;\n    background-color: @dropdown-link-hover-bg;\n  }\n}\n\n// Active state\n.dropdown-menu > .active > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-active-color;\n    text-decoration: none;\n    outline: 0;\n    background-color: @dropdown-link-active-bg;\n  }\n}\n\n// Disabled state\n//\n// Gray out text and ensure the hover/focus state remains gray\n\n.dropdown-menu > .disabled > a {\n  &,\n  &:hover,\n  &:focus {\n    color: @dropdown-link-disabled-color;\n  }\n}\n// Nuke hover/focus effects\n.dropdown-menu > .disabled > a {\n  &:hover,\n  &:focus {\n    text-decoration: none;\n    background-color: transparent;\n    background-image: none; // Remove CSS gradient\n    .reset-filter();\n    cursor: not-allowed;\n  }\n}\n\n// Open state for the dropdown\n.open {\n  // Show the menu\n  > .dropdown-menu {\n    display: block;\n  }\n\n  // Remove the outline when :focus is triggered\n  > a {\n    outline: 0;\n  }\n}\n\n// Menu positioning\n//\n// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown\n// menu with the parent.\n.dropdown-menu-right {\n  left: auto; // Reset the default from `.dropdown-menu`\n  right: 0;\n}\n// With v3, we enabled auto-flipping if you have a dropdown within a right\n// aligned nav component. To enable the undoing of that, we provide an override\n// to restore the default dropdown menu alignment.\n//\n// This is only for left-aligning a dropdown menu within a `.navbar-right` or\n// `.pull-right` nav component.\n.dropdown-menu-left {\n  left: 0;\n  right: auto;\n}\n\n// Dropdown section headers\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: @font-size-small;\n  line-height: @line-height-base;\n  color: @dropdown-header-color;\n}\n\n// Backdrop to catch body clicks on mobile, etc.\n.dropdown-backdrop {\n  position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  top: 0;\n  z-index: (@zindex-dropdown - 10);\n}\n\n// Right aligned dropdowns\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n//\n// Just add .dropup after the standard .dropdown class and you're set, bro.\n// TODO: abstract this so that the navbar fixed styles are not placed here?\n\n.dropup,\n.navbar-fixed-bottom .dropdown {\n  // Reverse the caret\n  .caret {\n    border-top: 0;\n    border-bottom: @caret-width-base solid;\n    content: \"\";\n  }\n  // Different positioning for bottom up menu\n  .dropdown-menu {\n    top: auto;\n    bottom: 100%;\n    margin-bottom: 1px;\n  }\n}\n\n\n// Component alignment\n//\n// Reiterate per navbar.less and the modified component alignment there.\n\n@media (min-width: @grid-float-breakpoint) {\n  .navbar-right {\n    .dropdown-menu {\n      .dropdown-menu-right();\n    }\n    // Necessary for overrides of the default right aligned menu.\n    // Will remove come v4 in all likelihood.\n    .dropdown-menu-left {\n      .dropdown-menu-left();\n    }\n  }\n}\n\n","//\n// Input groups\n// --------------------------------------------------\n\n// Base styles\n// -------------------------\n.input-group {\n  position: relative; // For dropdowns\n  display: table;\n  border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table\n\n  // Undo padding and float of grid classes\n  &[class*=\"col-\"] {\n    float: none;\n    padding-left: 0;\n    padding-right: 0;\n  }\n\n  .form-control {\n    // Ensure that the input is always above the *appended* addon button for\n    // proper border colors.\n    position: relative;\n    z-index: 2;\n\n    // IE9 fubars the placeholder attribute in text inputs and the arrows on\n    // select elements in input groups. To fix it, we float the input. Details:\n    // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855\n    float: left;\n\n    width: 100%;\n    margin-bottom: 0;\n  }\n}\n\n// Sizing options\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn { .input-lg(); }\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn { .input-sm(); }\n\n\n// Display as table-cell\n// -------------------------\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n  display: table-cell;\n\n  &:not(:first-child):not(:last-child) {\n    border-radius: 0;\n  }\n}\n// Addon and addon wrapper for buttons\n.input-group-addon,\n.input-group-btn {\n  width: 1%;\n  white-space: nowrap;\n  vertical-align: middle; // Match the inputs\n}\n\n// Text input groups\n// -------------------------\n.input-group-addon {\n  padding: @padding-base-vertical @padding-base-horizontal;\n  font-size: @font-size-base;\n  font-weight: normal;\n  line-height: 1;\n  color: @input-color;\n  text-align: center;\n  background-color: @input-group-addon-bg;\n  border: 1px solid @input-group-addon-border-color;\n  border-radius: @border-radius-base;\n\n  // Sizing\n  &.input-sm {\n    padding: @padding-small-vertical @padding-small-horizontal;\n    font-size: @font-size-small;\n    border-radius: @border-radius-small;\n  }\n  &.input-lg {\n    padding: @padding-large-vertical @padding-large-horizontal;\n    font-size: @font-size-large;\n    border-radius: @border-radius-large;\n  }\n\n  // Nuke default margins from checkboxes and radios to vertically center within.\n  input[type=\"radio\"],\n  input[type=\"checkbox\"] {\n    margin-top: 0;\n  }\n}\n\n// Reset rounded corners\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n  .border-right-radius(0);\n}\n.input-group-addon:first-child {\n  border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n  .border-left-radius(0);\n}\n.input-group-addon:last-child {\n  border-left: 0;\n}\n\n// Button input groups\n// -------------------------\n.input-group-btn {\n  position: relative;\n  // Jankily prevent input button groups from wrapping with `white-space` and\n  // `font-size` in combination with `inline-block` on buttons.\n  font-size: 0;\n  white-space: nowrap;\n\n  // Negative margin for spacing, position for bringing hovered/focused/actived\n  // element above the siblings.\n  > .btn {\n    position: relative;\n    + .btn {\n      margin-left: -1px;\n    }\n    // Bring the \"active\" button to the front\n    &:hover,\n    &:focus,\n    &:active {\n      z-index: 2;\n    }\n  }\n\n  // Negative margin to only have a 1px border between the two\n  &:first-child {\n    > .btn,\n    > .btn-group {\n      margin-right: -1px;\n    }\n  }\n  &:last-child {\n    > .btn,\n    > .btn-group {\n      margin-left: -1px;\n    }\n  }\n}\n","//\n// Navs\n// --------------------------------------------------\n\n\n// Base class\n// --------------------------------------------------\n\n.nav {\n  margin-bottom: 0;\n  padding-left: 0; // Override default ul/ol\n  list-style: none;\n  &:extend(.clearfix all);\n\n  > li {\n    position: relative;\n    display: block;\n\n    > a {\n      position: relative;\n      display: block;\n      padding: @nav-link-padding;\n      &:hover,\n      &:focus {\n        text-decoration: none;\n        background-color: @nav-link-hover-bg;\n      }\n    }\n\n    // Disabled state sets text to gray and nukes hover/tab effects\n    &.disabled > a {\n      color: @nav-disabled-link-color;\n\n      &:hover,\n      &:focus {\n        color: @nav-disabled-link-hover-color;\n        text-decoration: none;\n        background-color: transparent;\n        cursor: not-allowed;\n      }\n    }\n  }\n\n  // Open dropdowns\n  .open > a {\n    &,\n    &:hover,\n    &:focus {\n      background-color: @nav-link-hover-bg;\n      border-color: @link-color;\n    }\n  }\n\n  // Nav dividers (deprecated with v3.0.1)\n  //\n  // This should have been removed in v3 with the dropping of `.nav-list`, but\n  // we missed it. We don't currently support this anywhere, but in the interest\n  // of maintaining backward compatibility in case you use it, it's deprecated.\n  .nav-divider {\n    .nav-divider();\n  }\n\n  // Prevent IE8 from misplacing imgs\n  //\n  // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989\n  > li > a > img {\n    max-width: none;\n  }\n}\n\n\n// Tabs\n// -------------------------\n\n// Give the tabs something to sit on\n.nav-tabs {\n  border-bottom: 1px solid @nav-tabs-border-color;\n  > li {\n    float: left;\n    // Make the list-items overlay the bottom border\n    margin-bottom: -1px;\n\n    // Actual tabs (as links)\n    > a {\n      margin-right: 2px;\n      line-height: @line-height-base;\n      border: 1px solid transparent;\n      border-radius: @border-radius-base @border-radius-base 0 0;\n      &:hover {\n        border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color;\n      }\n    }\n\n    // Active state, and its :hover to override normal :hover\n    &.active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @nav-tabs-active-link-hover-color;\n        background-color: @nav-tabs-active-link-hover-bg;\n        border: 1px solid @nav-tabs-active-link-hover-border-color;\n        border-bottom-color: transparent;\n        cursor: default;\n      }\n    }\n  }\n  // pulling this in mainly for less shorthand\n  &.nav-justified {\n    .nav-justified();\n    .nav-tabs-justified();\n  }\n}\n\n\n// Pills\n// -------------------------\n.nav-pills {\n  > li {\n    float: left;\n\n    // Links rendered as pills\n    > a {\n      border-radius: @nav-pills-border-radius;\n    }\n    + li {\n      margin-left: 2px;\n    }\n\n    // Active state\n    &.active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @nav-pills-active-link-hover-color;\n        background-color: @nav-pills-active-link-hover-bg;\n      }\n    }\n  }\n}\n\n\n// Stacked pills\n.nav-stacked {\n  > li {\n    float: none;\n    + li {\n      margin-top: 2px;\n      margin-left: 0; // no need for this gap between nav items\n    }\n  }\n}\n\n\n// Nav variations\n// --------------------------------------------------\n\n// Justified nav links\n// -------------------------\n\n.nav-justified {\n  width: 100%;\n\n  > li {\n    float: none;\n     > a {\n      text-align: center;\n      margin-bottom: 5px;\n    }\n  }\n\n  > .dropdown .dropdown-menu {\n    top: auto;\n    left: auto;\n  }\n\n  @media (min-width: @screen-sm-min) {\n    > li {\n      display: table-cell;\n      width: 1%;\n      > a {\n        margin-bottom: 0;\n      }\n    }\n  }\n}\n\n// Move borders to anchors instead of bottom of list\n//\n// Mixin for adding on top the shared `.nav-justified` styles for our tabs\n.nav-tabs-justified {\n  border-bottom: 0;\n\n  > li > a {\n    // Override margin from .nav-tabs\n    margin-right: 0;\n    border-radius: @border-radius-base;\n  }\n\n  > .active > a,\n  > .active > a:hover,\n  > .active > a:focus {\n    border: 1px solid @nav-tabs-justified-link-border-color;\n  }\n\n  @media (min-width: @screen-sm-min) {\n    > li > a {\n      border-bottom: 1px solid @nav-tabs-justified-link-border-color;\n      border-radius: @border-radius-base @border-radius-base 0 0;\n    }\n    > .active > a,\n    > .active > a:hover,\n    > .active > a:focus {\n      border-bottom-color: @nav-tabs-justified-active-link-border-color;\n    }\n  }\n}\n\n\n// Tabbable tabs\n// -------------------------\n\n// Hide tabbable panes to start, show them when `.active`\n.tab-content {\n  > .tab-pane {\n    display: none;\n  }\n  > .active {\n    display: block;\n  }\n}\n\n\n// Dropdowns\n// -------------------------\n\n// Specific dropdowns\n.nav-tabs .dropdown-menu {\n  // make dropdown border overlap tab border\n  margin-top: -1px;\n  // Remove the top rounded corners here since there is a hard edge above the menu\n  .border-top-radius(0);\n}\n","//\n// Navbars\n// --------------------------------------------------\n\n\n// Wrapper and base class\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n  position: relative;\n  min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode)\n  margin-bottom: @navbar-margin-bottom;\n  border: 1px solid transparent;\n\n  // Prevent floats from breaking the navbar\n  &:extend(.clearfix all);\n\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: @navbar-border-radius;\n  }\n}\n\n\n// Navbar heading\n//\n// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy\n// styling of responsive aspects.\n\n.navbar-header {\n  &:extend(.clearfix all);\n\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n  }\n}\n\n\n// Navbar collapse (body)\n//\n// Group your navbar content into this for easy collapsing and expanding across\n// various device sizes. By default, this content is collapsed when <768px, but\n// will expand past that for a horizontal display.\n//\n// To start (on mobile devices) the navbar links, forms, and buttons are stacked\n// vertically and include a `max-height` to overflow in case you have too much\n// content for the user's viewport.\n\n.navbar-collapse {\n  max-height: @navbar-collapse-max-height;\n  overflow-x: visible;\n  padding-right: @navbar-padding-horizontal;\n  padding-left:  @navbar-padding-horizontal;\n  border-top: 1px solid transparent;\n  box-shadow: inset 0 1px 0 rgba(255,255,255,.1);\n  &:extend(.clearfix all);\n  -webkit-overflow-scrolling: touch;\n\n  &.in {\n    overflow-y: auto;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    width: auto;\n    border-top: 0;\n    box-shadow: none;\n\n    &.collapse {\n      display: block !important;\n      height: auto !important;\n      padding-bottom: 0; // Override default setting\n      overflow: visible !important;\n    }\n\n    &.in {\n      overflow-y: visible;\n    }\n\n    // Undo the collapse side padding for navbars with containers to ensure\n    // alignment of right-aligned contents.\n    .navbar-fixed-top &,\n    .navbar-static-top &,\n    .navbar-fixed-bottom & {\n      padding-left: 0;\n      padding-right: 0;\n    }\n  }\n}\n\n\n// Both navbar header and collapse\n//\n// When a container is present, change the behavior of the header and collapse.\n\n.container,\n.container-fluid {\n  > .navbar-header,\n  > .navbar-collapse {\n    margin-right: -@navbar-padding-horizontal;\n    margin-left:  -@navbar-padding-horizontal;\n\n    @media (min-width: @grid-float-breakpoint) {\n      margin-right: 0;\n      margin-left:  0;\n    }\n  }\n}\n\n\n//\n// Navbar alignment options\n//\n// Display the navbar across the entirety of the page or fixed it to the top or\n// bottom of the page.\n\n// Static top (unfixed, but 100% wide) navbar\n.navbar-static-top {\n  z-index: @zindex-navbar;\n  border-width: 0 0 1px;\n\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: 0;\n  }\n}\n\n// Fix the top/bottom navbars when screen real estate supports it\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  position: fixed;\n  right: 0;\n  left: 0;\n  z-index: @zindex-navbar-fixed;\n\n  // Undo the rounded corners\n  @media (min-width: @grid-float-breakpoint) {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top {\n  top: 0;\n  border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n  bottom: 0;\n  margin-bottom: 0; // override .navbar defaults\n  border-width: 1px 0 0;\n}\n\n\n// Brand/project name\n\n.navbar-brand {\n  float: left;\n  padding: @navbar-padding-vertical @navbar-padding-horizontal;\n  font-size: @font-size-large;\n  line-height: @line-height-computed;\n  height: @navbar-height;\n\n  &:hover,\n  &:focus {\n    text-decoration: none;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    .navbar > .container &,\n    .navbar > .container-fluid & {\n      margin-left: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Navbar toggle\n//\n// Custom button for toggling the `.navbar-collapse`, powered by the collapse\n// JavaScript plugin.\n\n.navbar-toggle {\n  position: relative;\n  float: right;\n  margin-right: @navbar-padding-horizontal;\n  padding: 9px 10px;\n  .navbar-vertical-align(34px);\n  background-color: transparent;\n  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n  border: 1px solid transparent;\n  border-radius: @border-radius-base;\n\n  // We remove the `outline` here, but later compensate by attaching `:hover`\n  // styles to `:focus`.\n  &:focus {\n    outline: none;\n  }\n\n  // Bars\n  .icon-bar {\n    display: block;\n    width: 22px;\n    height: 2px;\n    border-radius: 1px;\n  }\n  .icon-bar + .icon-bar {\n    margin-top: 4px;\n  }\n\n  @media (min-width: @grid-float-breakpoint) {\n    display: none;\n  }\n}\n\n\n// Navbar nav links\n//\n// Builds on top of the `.nav` components with its own modifier class to make\n// the nav the full height of the horizontal nav (above 768px).\n\n.navbar-nav {\n  margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal;\n\n  > li > a {\n    padding-top:    10px;\n    padding-bottom: 10px;\n    line-height: @line-height-computed;\n  }\n\n  @media (max-width: @grid-float-breakpoint-max) {\n    // Dropdowns get custom display when collapsed\n    .open .dropdown-menu {\n      position: static;\n      float: none;\n      width: auto;\n      margin-top: 0;\n      background-color: transparent;\n      border: 0;\n      box-shadow: none;\n      > li > a,\n      .dropdown-header {\n        padding: 5px 15px 5px 25px;\n      }\n      > li > a {\n        line-height: @line-height-computed;\n        &:hover,\n        &:focus {\n          background-image: none;\n        }\n      }\n    }\n  }\n\n  // Uncollapse the nav\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n    margin: 0;\n\n    > li {\n      float: left;\n      > a {\n        padding-top:    @navbar-padding-vertical;\n        padding-bottom: @navbar-padding-vertical;\n      }\n    }\n\n    &.navbar-right:last-child {\n      margin-right: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Component alignment\n//\n// Repurpose the pull utilities as their own navbar utilities to avoid specificity\n// issues with parents and chaining. Only do this when the navbar is uncollapsed\n// though so that navbar contents properly stack and align in mobile.\n\n@media (min-width: @grid-float-breakpoint) {\n  .navbar-left  { .pull-left(); }\n  .navbar-right { .pull-right(); }\n}\n\n\n// Navbar form\n//\n// Extension of the `.form-inline` with some extra flavor for optimum display in\n// our navbars.\n\n.navbar-form {\n  margin-left: -@navbar-padding-horizontal;\n  margin-right: -@navbar-padding-horizontal;\n  padding: 10px @navbar-padding-horizontal;\n  border-top: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n  @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);\n  .box-shadow(@shadow);\n\n  // Mixin behavior for optimum display\n  .form-inline();\n\n  .form-group {\n    @media (max-width: @grid-float-breakpoint-max) {\n      margin-bottom: 5px;\n    }\n  }\n\n  // Vertically center in expanded, horizontal navbar\n  .navbar-vertical-align(@input-height-base);\n\n  // Undo 100% width for pull classes\n  @media (min-width: @grid-float-breakpoint) {\n    width: auto;\n    border: 0;\n    margin-left: 0;\n    margin-right: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n    .box-shadow(none);\n\n    // Outdent the form if last child to line up with content down the page\n    &.navbar-right:last-child {\n      margin-right: -@navbar-padding-horizontal;\n    }\n  }\n}\n\n\n// Dropdown menus\n\n// Menu position and menu carets\n.navbar-nav > li > .dropdown-menu {\n  margin-top: 0;\n  .border-top-radius(0);\n}\n// Menu position and menu caret support for dropups via extra dropup class\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n  .border-bottom-radius(0);\n}\n\n\n// Buttons in navbars\n//\n// Vertically center a button within a navbar (when *not* in a form).\n\n.navbar-btn {\n  .navbar-vertical-align(@input-height-base);\n\n  &.btn-sm {\n    .navbar-vertical-align(@input-height-small);\n  }\n  &.btn-xs {\n    .navbar-vertical-align(22);\n  }\n}\n\n\n// Text in navbars\n//\n// Add a class to make any element properly align itself vertically within the navbars.\n\n.navbar-text {\n  .navbar-vertical-align(@line-height-computed);\n\n  @media (min-width: @grid-float-breakpoint) {\n    float: left;\n    margin-left: @navbar-padding-horizontal;\n    margin-right: @navbar-padding-horizontal;\n\n    // Outdent the form if last child to line up with content down the page\n    &.navbar-right:last-child {\n      margin-right: 0;\n    }\n  }\n}\n\n// Alternate navbars\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n  background-color: @navbar-default-bg;\n  border-color: @navbar-default-border;\n\n  .navbar-brand {\n    color: @navbar-default-brand-color;\n    &:hover,\n    &:focus {\n      color: @navbar-default-brand-hover-color;\n      background-color: @navbar-default-brand-hover-bg;\n    }\n  }\n\n  .navbar-text {\n    color: @navbar-default-color;\n  }\n\n  .navbar-nav {\n    > li > a {\n      color: @navbar-default-link-color;\n\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-hover-color;\n        background-color: @navbar-default-link-hover-bg;\n      }\n    }\n    > .active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-active-color;\n        background-color: @navbar-default-link-active-bg;\n      }\n    }\n    > .disabled > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-default-link-disabled-color;\n        background-color: @navbar-default-link-disabled-bg;\n      }\n    }\n  }\n\n  .navbar-toggle {\n    border-color: @navbar-default-toggle-border-color;\n    &:hover,\n    &:focus {\n      background-color: @navbar-default-toggle-hover-bg;\n    }\n    .icon-bar {\n      background-color: @navbar-default-toggle-icon-bar-bg;\n    }\n  }\n\n  .navbar-collapse,\n  .navbar-form {\n    border-color: @navbar-default-border;\n  }\n\n  // Dropdown menu items\n  .navbar-nav {\n    // Remove background color from open dropdown\n    > .open > a {\n      &,\n      &:hover,\n      &:focus {\n        background-color: @navbar-default-link-active-bg;\n        color: @navbar-default-link-active-color;\n      }\n    }\n\n    @media (max-width: @grid-float-breakpoint-max) {\n      // Dropdowns get custom display when collapsed\n      .open .dropdown-menu {\n        > li > a {\n          color: @navbar-default-link-color;\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-hover-color;\n            background-color: @navbar-default-link-hover-bg;\n          }\n        }\n        > .active > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-active-color;\n            background-color: @navbar-default-link-active-bg;\n          }\n        }\n        > .disabled > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-default-link-disabled-color;\n            background-color: @navbar-default-link-disabled-bg;\n          }\n        }\n      }\n    }\n  }\n\n\n  // Links in navbars\n  //\n  // Add a class to ensure links outside the navbar nav are colored correctly.\n\n  .navbar-link {\n    color: @navbar-default-link-color;\n    &:hover {\n      color: @navbar-default-link-hover-color;\n    }\n  }\n\n}\n\n// Inverse navbar\n\n.navbar-inverse {\n  background-color: @navbar-inverse-bg;\n  border-color: @navbar-inverse-border;\n\n  .navbar-brand {\n    color: @navbar-inverse-brand-color;\n    &:hover,\n    &:focus {\n      color: @navbar-inverse-brand-hover-color;\n      background-color: @navbar-inverse-brand-hover-bg;\n    }\n  }\n\n  .navbar-text {\n    color: @navbar-inverse-color;\n  }\n\n  .navbar-nav {\n    > li > a {\n      color: @navbar-inverse-link-color;\n\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-hover-color;\n        background-color: @navbar-inverse-link-hover-bg;\n      }\n    }\n    > .active > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-active-color;\n        background-color: @navbar-inverse-link-active-bg;\n      }\n    }\n    > .disabled > a {\n      &,\n      &:hover,\n      &:focus {\n        color: @navbar-inverse-link-disabled-color;\n        background-color: @navbar-inverse-link-disabled-bg;\n      }\n    }\n  }\n\n  // Darken the responsive nav toggle\n  .navbar-toggle {\n    border-color: @navbar-inverse-toggle-border-color;\n    &:hover,\n    &:focus {\n      background-color: @navbar-inverse-toggle-hover-bg;\n    }\n    .icon-bar {\n      background-color: @navbar-inverse-toggle-icon-bar-bg;\n    }\n  }\n\n  .navbar-collapse,\n  .navbar-form {\n    border-color: darken(@navbar-inverse-bg, 7%);\n  }\n\n  // Dropdowns\n  .navbar-nav {\n    > .open > a {\n      &,\n      &:hover,\n      &:focus {\n        background-color: @navbar-inverse-link-active-bg;\n        color: @navbar-inverse-link-active-color;\n      }\n    }\n\n    @media (max-width: @grid-float-breakpoint-max) {\n      // Dropdowns get custom display\n      .open .dropdown-menu {\n        > .dropdown-header {\n          border-color: @navbar-inverse-border;\n        }\n        .divider {\n          background-color: @navbar-inverse-border;\n        }\n        > li > a {\n          color: @navbar-inverse-link-color;\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-hover-color;\n            background-color: @navbar-inverse-link-hover-bg;\n          }\n        }\n        > .active > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-active-color;\n            background-color: @navbar-inverse-link-active-bg;\n          }\n        }\n        > .disabled > a {\n          &,\n          &:hover,\n          &:focus {\n            color: @navbar-inverse-link-disabled-color;\n            background-color: @navbar-inverse-link-disabled-bg;\n          }\n        }\n      }\n    }\n  }\n\n  .navbar-link {\n    color: @navbar-inverse-link-color;\n    &:hover {\n      color: @navbar-inverse-link-hover-color;\n    }\n  }\n\n}\n","//\n// Utility classes\n// --------------------------------------------------\n\n\n// Floats\n// -------------------------\n\n.clearfix {\n  .clearfix();\n}\n.center-block {\n  .center-block();\n}\n.pull-right {\n  float: right !important;\n}\n.pull-left {\n  float: left !important;\n}\n\n\n// Toggling content\n// -------------------------\n\n// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1\n.hide {\n  display: none !important;\n}\n.show {\n  display: block !important;\n}\n.invisible {\n  visibility: hidden;\n}\n.text-hide {\n  .text-hide();\n}\n\n\n// Hide from screenreaders and browsers\n//\n// Credit: HTML5 Boilerplate\n\n.hidden {\n  display: none !important;\n  visibility: hidden !important;\n}\n\n\n// For Affix plugin\n// -------------------------\n\n.affix {\n  position: fixed;\n}\n","//\n// Breadcrumbs\n// --------------------------------------------------\n\n\n.breadcrumb {\n  padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal;\n  margin-bottom: @line-height-computed;\n  list-style: none;\n  background-color: @breadcrumb-bg;\n  border-radius: @border-radius-base;\n\n  > li {\n    display: inline-block;\n\n    + li:before {\n      content: \"@{breadcrumb-separator}\\00a0\"; // Unicode space added since inline-block means non-collapsing white-space\n      padding: 0 5px;\n      color: @breadcrumb-color;\n    }\n  }\n\n  > .active {\n    color: @breadcrumb-active-color;\n  }\n}\n","//\n// Pagination (multiple pages)\n// --------------------------------------------------\n.pagination {\n  display: inline-block;\n  padding-left: 0;\n  margin: @line-height-computed 0;\n  border-radius: @border-radius-base;\n\n  > li {\n    display: inline; // Remove list-style and block-level defaults\n    > a,\n    > span {\n      position: relative;\n      float: left; // Collapse white-space\n      padding: @padding-base-vertical @padding-base-horizontal;\n      line-height: @line-height-base;\n      text-decoration: none;\n      color: @pagination-color;\n      background-color: @pagination-bg;\n      border: 1px solid @pagination-border;\n      margin-left: -1px;\n    }\n    &:first-child {\n      > a,\n      > span {\n        margin-left: 0;\n        .border-left-radius(@border-radius-base);\n      }\n    }\n    &:last-child {\n      > a,\n      > span {\n        .border-right-radius(@border-radius-base);\n      }\n    }\n  }\n\n  > li > a,\n  > li > span {\n    &:hover,\n    &:focus {\n      color: @pagination-hover-color;\n      background-color: @pagination-hover-bg;\n      border-color: @pagination-hover-border;\n    }\n  }\n\n  > .active > a,\n  > .active > span {\n    &,\n    &:hover,\n    &:focus {\n      z-index: 2;\n      color: @pagination-active-color;\n      background-color: @pagination-active-bg;\n      border-color: @pagination-active-border;\n      cursor: default;\n    }\n  }\n\n  > .disabled {\n    > span,\n    > span:hover,\n    > span:focus,\n    > a,\n    > a:hover,\n    > a:focus {\n      color: @pagination-disabled-color;\n      background-color: @pagination-disabled-bg;\n      border-color: @pagination-disabled-border;\n      cursor: not-allowed;\n    }\n  }\n}\n\n// Sizing\n// --------------------------------------------------\n\n// Large\n.pagination-lg {\n  .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @border-radius-large);\n}\n\n// Small\n.pagination-sm {\n  .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @border-radius-small);\n}\n","//\n// Pager pagination\n// --------------------------------------------------\n\n\n.pager {\n  padding-left: 0;\n  margin: @line-height-computed 0;\n  list-style: none;\n  text-align: center;\n  &:extend(.clearfix all);\n  li {\n    display: inline;\n    > a,\n    > span {\n      display: inline-block;\n      padding: 5px 14px;\n      background-color: @pager-bg;\n      border: 1px solid @pager-border;\n      border-radius: @pager-border-radius;\n    }\n\n    > a:hover,\n    > a:focus {\n      text-decoration: none;\n      background-color: @pager-hover-bg;\n    }\n  }\n\n  .next {\n    > a,\n    > span {\n      float: right;\n    }\n  }\n\n  .previous {\n    > a,\n    > span {\n      float: left;\n    }\n  }\n\n  .disabled {\n    > a,\n    > a:hover,\n    > a:focus,\n    > span {\n      color: @pager-disabled-color;\n      background-color: @pager-bg;\n      cursor: not-allowed;\n    }\n  }\n\n}\n","//\n// Labels\n// --------------------------------------------------\n\n.label {\n  display: inline;\n  padding: .2em .6em .3em;\n  font-size: 75%;\n  font-weight: bold;\n  line-height: 1;\n  color: @label-color;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: .25em;\n\n  // Add hover effects, but only for links\n  &[href] {\n    &:hover,\n    &:focus {\n      color: @label-link-hover-color;\n      text-decoration: none;\n      cursor: pointer;\n    }\n  }\n\n  // Empty labels collapse automatically (not available in IE8)\n  &:empty {\n    display: none;\n  }\n\n  // Quick fix for labels in buttons\n  .btn & {\n    position: relative;\n    top: -1px;\n  }\n}\n\n// Colors\n// Contextual variations (linked labels get darker on :hover)\n\n.label-default {\n  .label-variant(@label-default-bg);\n}\n\n.label-primary {\n  .label-variant(@label-primary-bg);\n}\n\n.label-success {\n  .label-variant(@label-success-bg);\n}\n\n.label-info {\n  .label-variant(@label-info-bg);\n}\n\n.label-warning {\n  .label-variant(@label-warning-bg);\n}\n\n.label-danger {\n  .label-variant(@label-danger-bg);\n}\n","//\n// Badges\n// --------------------------------------------------\n\n\n// Base classes\n.badge {\n  display: inline-block;\n  min-width: 10px;\n  padding: 3px 7px;\n  font-size: @font-size-small;\n  font-weight: @badge-font-weight;\n  color: @badge-color;\n  line-height: @badge-line-height;\n  vertical-align: baseline;\n  white-space: nowrap;\n  text-align: center;\n  background-color: @badge-bg;\n  border-radius: @badge-border-radius;\n\n  // Empty badges collapse automatically (not available in IE8)\n  &:empty {\n    display: none;\n  }\n\n  // Quick fix for badges in buttons\n  .btn & {\n    position: relative;\n    top: -1px;\n  }\n  .btn-xs & {\n    top: 0;\n    padding: 1px 5px;\n  }\n}\n\n// Hover state, but only for links\na.badge {\n  &:hover,\n  &:focus {\n    color: @badge-link-hover-color;\n    text-decoration: none;\n    cursor: pointer;\n  }\n}\n\n// Account for counters in navs\na.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n  color: @badge-active-color;\n  background-color: @badge-active-bg;\n}\n.nav-pills > li > a > .badge {\n  margin-left: 3px;\n}\n","//\n// Jumbotron\n// --------------------------------------------------\n\n\n.jumbotron {\n  padding: @jumbotron-padding;\n  margin-bottom: @jumbotron-padding;\n  color: @jumbotron-color;\n  background-color: @jumbotron-bg;\n\n  h1,\n  .h1 {\n    color: @jumbotron-heading-color;\n  }\n  p {\n    margin-bottom: (@jumbotron-padding / 2);\n    font-size: @jumbotron-font-size;\n    font-weight: 200;\n  }\n\n  .container & {\n    border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container\n  }\n\n  .container {\n    max-width: 100%;\n  }\n\n  @media screen and (min-width: @screen-sm-min) {\n    padding-top:    (@jumbotron-padding * 1.6);\n    padding-bottom: (@jumbotron-padding * 1.6);\n\n    .container & {\n      padding-left:  (@jumbotron-padding * 2);\n      padding-right: (@jumbotron-padding * 2);\n    }\n\n    h1,\n    .h1 {\n      font-size: (@font-size-base * 4.5);\n    }\n  }\n}\n","//\n// Alerts\n// --------------------------------------------------\n\n\n// Base styles\n// -------------------------\n\n.alert {\n  padding: @alert-padding;\n  margin-bottom: @line-height-computed;\n  border: 1px solid transparent;\n  border-radius: @alert-border-radius;\n\n  // Headings for larger alerts\n  h4 {\n    margin-top: 0;\n    // Specified for the h4 to prevent conflicts of changing @headings-color\n    color: inherit;\n  }\n  // Provide class for links that match alerts\n  .alert-link {\n    font-weight: @alert-link-font-weight;\n  }\n\n  // Improve alignment and spacing of inner content\n  > p,\n  > ul {\n    margin-bottom: 0;\n  }\n  > p + p {\n    margin-top: 5px;\n  }\n}\n\n// Dismissable alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissable {\n padding-right: (@alert-padding + 20);\n\n  // Adjust close link position\n  .close {\n    position: relative;\n    top: -2px;\n    right: -21px;\n    color: inherit;\n  }\n}\n\n// Alternate styles\n//\n// Generate contextual modifier classes for colorizing the alert.\n\n.alert-success {\n  .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text);\n}\n.alert-info {\n  .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text);\n}\n.alert-warning {\n  .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text);\n}\n.alert-danger {\n  .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text);\n}\n","//\n// Progress bars\n// --------------------------------------------------\n\n\n// Bar animations\n// -------------------------\n\n// WebKit\n@-webkit-keyframes progress-bar-stripes {\n  from  { background-position: 40px 0; }\n  to    { background-position: 0 0; }\n}\n\n// Spec and IE10+\n@keyframes progress-bar-stripes {\n  from  { background-position: 40px 0; }\n  to    { background-position: 0 0; }\n}\n\n\n\n// Bar itself\n// -------------------------\n\n// Outer container\n.progress {\n  overflow: hidden;\n  height: @line-height-computed;\n  margin-bottom: @line-height-computed;\n  background-color: @progress-bg;\n  border-radius: @border-radius-base;\n  .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));\n}\n\n// Bar of progress\n.progress-bar {\n  float: left;\n  width: 0%;\n  height: 100%;\n  font-size: @font-size-small;\n  line-height: @line-height-computed;\n  color: @progress-bar-color;\n  text-align: center;\n  background-color: @progress-bar-bg;\n  .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));\n  .transition(width .6s ease);\n}\n\n// Striped bars\n.progress-striped .progress-bar {\n  #gradient > .striped();\n  background-size: 40px 40px;\n}\n\n// Call animation for the active one\n.progress.active .progress-bar {\n  .animation(progress-bar-stripes 2s linear infinite);\n}\n\n\n\n// Variations\n// -------------------------\n\n.progress-bar-success {\n  .progress-bar-variant(@progress-bar-success-bg);\n}\n\n.progress-bar-info {\n  .progress-bar-variant(@progress-bar-info-bg);\n}\n\n.progress-bar-warning {\n  .progress-bar-variant(@progress-bar-warning-bg);\n}\n\n.progress-bar-danger {\n  .progress-bar-variant(@progress-bar-danger-bg);\n}\n","// Media objects\n// Source: http://stubbornella.org/content/?p=497\n// --------------------------------------------------\n\n\n// Common styles\n// -------------------------\n\n// Clear the floats\n.media,\n.media-body {\n  overflow: hidden;\n  zoom: 1;\n}\n\n// Proper spacing between instances of .media\n.media,\n.media .media {\n  margin-top: 15px;\n}\n.media:first-child {\n  margin-top: 0;\n}\n\n// For images and videos, set to block\n.media-object {\n  display: block;\n}\n\n// Reset margins on headings for tighter default spacing\n.media-heading {\n  margin: 0 0 5px;\n}\n\n\n// Media image alignment\n// -------------------------\n\n.media {\n  > .pull-left {\n    margin-right: 10px;\n  }\n  > .pull-right {\n    margin-left: 10px;\n  }\n}\n\n\n// Media list variation\n// -------------------------\n\n// Undo default ul/ol styles\n.media-list {\n  padding-left: 0;\n  list-style: none;\n}\n","//\n// List groups\n// --------------------------------------------------\n\n\n// Base class\n//\n// Easily usable on