{ "info": { "author": "Mark S. Weiss", "author_email": "marksimonweiss@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", "Topic :: Utilities" ], "description": "What Problem Does sofine solve?\n-------------------------------\n\nYou need to get data related to a set of keys from many sources: web\nscrapers, Web APIs, flat files, data stores. Wouldn't it be nice to\nbuild one combined data set over multiple calls with one command line,\nREST or Python call? Wouldn't it be great if each data retrieval script\nyou wrote was a reusable plugin that you could combine with any other?\n\nYou need a \"glue API.\"\n\nThis is the problem ``sofine`` solves. It's a small enough problem that\nyou could solve it yourself. But ``sofine`` is minimal to deploy and\nwrite plugins for, and has already decided in an optimally flexible way\nthe same design decisions you would have to make if you wrote this\nyourself.\n\nFeatures\n--------\n\n1. Do (almost) no more work than if you wrote one-off data collection\n scripts\n2. Manage your data retrieval plugins in any directory with any\n directory structure you like\n3. Call plugins from the command line, as REST resources or from Python\n4. Chain as many plugin calls as you want together and get back one JSON\n data set with all the data collected from all the chained calls\n5. If called from the command line, ``sofine`` reads data from ``stdin``\n if it is present, and always outputs to ``stdout``. So ``sofine``\n piped calls can themselves be composed in larger piped expressions.\n\nFor fun, here is an example of features 4 and 5, combining a ``sofine``\npipeline with the fantastic `JSON query tool\njq `__ for further filtering.\n\n::\n\n echo '{\"AAPL\":[]}' | python $PYTHONPATH/sofine/runner.py '--SF-s ystockquotelib --SF-g example | --SF-s google_search_results --SF-g example' | jq 'map(recurse(.results) | {titleNoFormatting}'\n\nOverview\n--------\n\nTo get started, you:\n\n1. ``pip install sofine``\n2. Make sure your ``$PYTHONPATH`` points to the package directory where\n pip installed ``sofine``\n3. Create a plugin directory and assign it's path to the environment\n ``SOFINE_PLUGIN_PATH``\n4. Write and call some data retrieval plugins (or just start using the\n included ones)\n\nPlugins require two attributes and one method in the simple case and\nthree methods in the most elaborate edge case. You can optionally define\ntwo additional attributes for clients to use to introspect your plugins.\n\n``sofine`` ships with a few useful plugins to get you started and give\nyou the idea; you can combine these with your custom plugins with no\nadditional configuration or code. The included plugins are:\n\n- ``sofine.plugins.standard.file_source`` - Retrieves keys from a JSON\n file to add to the data set being built. See\n `here `__\n for the details.\n- ``example.archive_dot_org_search_results`` - Takes a search query and\n returns results from www.archive.org\n- ``example.google_search_results`` - Takes a search query and returns\n results from the Google Search API\n- ``example.fidelity`` - Takes a userId, pin, accountId and email, logs\n into Fidelity, scrapes the account portfolio and returns the tickers\n found as keys and four attributes of data for each ticker\n- ``example.ystockquotelib`` - Takes a list of tickers and returns the\n data available from Yahoo! Finance for each ticker\n\nHere is what usage looks like ...\n\nFrom the command line:\n\n::\n\n $ echo '{\"AAPL\":[]}' | python $PYTHONPATH/sofine/runner.py '--SF-s ystockquotelib --SF-g example | --SF-s google_search_results --SF-g example'\n\nREST-fully:\n\n::\n\n $ python $PYTHONPATH/sofine/rest_runner.py\n\n $ curl -X POST -d '{\"AAPL\":[]}' --header \"Content-Type:application/json\" http://localhost:10000/SF-s/ystockquotelib/SF-g/example/SF-s/google_search_results/SF-g/example\n\nFrom Python:\n\n::\n\n import sofine.runner as runner\n\n data = {\"AAPL\": []}\n data_sources = ['ystockquotelib', 'google_search_results']\n data_source_groups = ['example', 'example']\n data_source_args = [[], []]\n data = runner.get_data_batch(data, data_sources, data_source_groups, data_source_args)\n\nAll three calling styles return the same data set. ``sofine`` data sets\nmap string keys to arrays of attributes, which are Python dicts. By\ndefault, these are returned as JSON to stdout. ``sofine`` also ships\nwith support for CSV, and you can write your own data format plugins\n(more on that below).\n\nHere is an example retrieved using included data retrieval plugins: the\nkey \"AAPL,\" with all the attributes retrieved from Yahoo! Finance and\nthe Google Search API combined.\n\n::\n\n {\n \"AAPL\": \n [\n {\n \"results\": [\n {\n \"GsearchResultClass\": \"GwebSearch\",\n \"cacheUrl\": \"http://www.google.com/search?q=cache:XhbIlCyrcXMJ:finance.yahoo.com\",\n \"content\": \"View the basic AAPL stock chart on Yahoo! Finance. Change the date range, chart type and compare Apple Inc. against other companies.\",\n \"title\": \"AAPL: Summary for Apple Inc.- Yahoo! Finance\",\n \"titleNoFormatting\": \"AAPL: Summary for Apple Inc.- Yahoo! Finance\",\n \"unescapedUrl\": \"http://finance.yahoo.com/q?s=AAPL\",\n \"url\": \"http://finance.yahoo.com/q%3Fs%3DAAPL\",\n \"visibleUrl\": \"finance.yahoo.com\"\n },\n ...\n ...\n ]\n },\n {\"avg_daily_volume\": \"59390100\"},\n {\"book_value\": \"20.193\"},\n {\"change\": \"+1.349\"},\n {\"dividend_per_share\": \"1.7771\"},\n {\"dividend_yield\": \"1.82\"},\n {\"earnings_per_share\": \"6.20\"},\n {\"ebitda\": \"59.128B\"},\n {\"fifty_day_moving_avg\": \"93.8151\"},\n {\"fifty_two_week_high\": \"99.24\"},\n {\"fifty_two_week_low\": \"63.8886\"},\n {\"market_cap\": \"592.9B\"},\n {\"price\": \"99.02\"},\n {\"price_book_ratio\": \"4.84\"},\n {\"price_earnings_growth_ratio\": \"1.26\"},\n {\"price_earnings_ratio\": \"15.75\"},\n {\"price_sales_ratio\": \"3.28\"},\n {\"short_ratio\": \"1.70\"},\n {\"stock_exchange\": \"\\\"NasdaqNM\\\"\"},\n {\"two_hundred_day_moving_avg\": \"82.8458\"},\n {\"volume\": \"55317688\"}\n ]\n } \n\nInstalling sofine\n-----------------\n\n::\n\n pip install sofine \n\nThen, make sure your ``$PYTHONPATH`` variable is set and points to the\nsite-packages directory of your Python where pip installed ``sofine``.\n\n::\n\n export PYTHONPATH=\n\nThen, create a plugin directory and assign its path to an environment\nvariable ``SOFINE_PLUGIN_PATH``. You probably want to add it to your\nshell configuration file.\n\n::\n\n export SOFINE_PLUGIN_PATH=\n\n``sofine`` runs its REST server on port 10000. If you want to use a\ndifferent port, set the environment variable ``SOFINE_REST_PORT``. You\nprobably want to add it to your shell configuration file.\n\n::\n\n export SOFINE_REST_PORT=\n\nIf you are going to create data format plugins, create a data format\nlugin directory and assign its path to the environment variable\n``SOFINE_DATA_FORMAT_PLUGIN_PATH``.\n\n::\n\n export SOFINE_DATA_FORMAT_PLUGIN_PATH=\n\nIf you want to use the included ``fidelity`` and ``ystockquotelib``\nplugins in the ``plugins.examples`` plugin group, also install the\nfollowing:\n\n::\n\n easy_install mechanize\n easy_install beautifulsoup4\n pip install ystockquote\n\nTwo Kinds of Plugins: Data Retrieval and Data Format\n----------------------------------------------------\n\n``sofine`` uses two kinds of plugins. *Data retrieval plugins* are what\nyou call singly or in chained expressions to return data sets. When the\ndocumentation says \"plugin,\" it means data retrieval plugin. But\n``sofine`` also supports plugins for the data format of data sets. By\ndefault ``sofine`` expects input on ``stdin`` in JSON format and writes\nJSON to ``stdout``. But there is also a plugin for CSV.\n\nHow Python Data Retrieval Plugins Work and How to Write Them\n------------------------------------------------------------\n\nBoilerplate\n~~~~~~~~~~~\n\nAll plugins inherit from a super class,\n``sofine.plugins.plugin_base.PluginBase``. Your plugin ``__init__``\nmethod must call the super class ``__init__``.\n\n::\n\n class ArchiveDotOrgSearchResults(plugin_base.PluginBase):\n def __init__(self):\n super(ArchiveDotOrgSearchResults, self).__init__()\n\nThe last line of your plugin should assign the module-scope variable\n``plugin`` to the name of your plugin class. For example:\n\n::\n\n plugin = ArchiveDotOrgResults \n\nPlugin Attributes\n~~~~~~~~~~~~~~~~~\n\nThe base class defines four attributes:\n\n- ``self.name`` - ``string``. The name of the plugin\n- ``self.group`` - ``string``. The pluging group of the plugin. This\n the subdirectory in the plugin directory into which the plugin is\n deployed.\n- ``self.schema`` - ``list of string``. The set of attribute keys that\n calls to ``get_data`` can associate with a key passed to\n ``get_data``.\n- ``self.adds_keys`` - ``boolean``. Indicates whether the plugin adds\n keys to the data set being built or only adds attributes to existing\n keys.\n\nYou must always define ``name`` and ``group``.\n\n``name``\n^^^^^^^^\n\n``name`` must match the module name of the plugin module, that is the\nname you would use in an ``import`` statement.\n\n``group``\n^^^^^^^^^\n\n``group`` must match the name of the subdirectory of your plugin\ndirectory where the plugin is deployed. ``sofine`` uses ``name`` and\n``group`` to load and run your plugin, so they have to be there and they\nhave to be correct.\n\n``schema``\n^^^^^^^^^^\n\n``schema`` is optional. It allows users of your plugin to introspect it.\n\n``schema`` is a list of strings that tells a client of your plugin the\nset of possible attribute keys that your plugin returns for each key it\nrecieves. For example, if your plugin takes stock tickers as keys and\nlooks up a current quote, its ``schema`` declaration might look like\nthis:\n\n::\n\n self.schema = ['quote']\n\n``adds_keys``\n^^^^^^^^^^^^^\n\n``adds_keys`` lets users ask your plugin if it adds keys to the data set\nbeing built when ``sofine`` calls it, or if it just adds attributes for\nthe keys it receives.\n\nFor example, the ``ystockquotelib`` plugin in the\n``sofine.plugins.example`` group takes a set of stock tikckers as keys\nand retrieves the available data for each of them from Yahoo! Finance.\nThis plugin has the attribute declaration ``self.adds_keys = False``. On\nthe other hand, the ``sofine.plugins.fidelity`` plugin is a scraper that\ncan log into the Fidelity, go to the portfolio page for the logged in\nuser, scrape all the tickers for the securities in that portfolio, and\nadd those keys and whatever data it finds to the data set being built.\nThis plugin has a value of ``True`` for ``adds_keys``.\n\nPlugin Methods\n~~~~~~~~~~~~~~\n\nPlugins also have four methods.\n\n``get_data``\n^^^^^^^^^^^^\n\n``get_data`` is not implemented in the base class and must be\nimplemented by you in your plugin.\n\nThis method takes a list of keys and a list of arguments. It must return\na dict whose keys are a proper superset of the keys it received (the\nreturn set of keys can have more keys than were passed to ``get_data``\nif the plugin adds keys). This dict must have string keys and a dict\nvalue for each key. The dict value is the data retrieved for each key.\nThe keys in that dict must be a set of strings that is a proper subset\nof the set of strings in ``self.schema``.\n\nHere is an example of ``get_data`` from the ``sofine`` plugin\n``sofine.plugins.example.ystockquotelib``.\n\n::\n\n def get_data(self, keys, args):\n \"\"\"\n * `keys` - `list`. The list of keys to process.\n * `args` - `'list`. Empty for this plugin.\n Calls the Yahoo API to get all available fields for each ticker provided as a key in `keys`.\"\"\"\n return {ticker : ystockquote.get_all(ticker) for ticker in keys}\n\n``get_namespaced_data``\n^^^^^^^^^^^^^^^^^^^^^^^\n\nA wrapper around ``get_data`` provided by ``sofine``, which return the\nsame data with attribute keys wrapped in a namespace of the plugin group\nand name. So our example ``quote`` attribute above would look like this\nin the returned data set:\n\n::\n\n {\"trading::get_quotes::quote\" : 47.65}\n\n``parse_args``\n^^^^^^^^^^^^^^\n\nThe other method you will often need to implement is ``parse_args``. If\nyour ``get_data`` requires no arguments you need not implement\n``parse_args``. But if your ``get_data`` call requires arguments, you\nmust implement ``parse_args``. The method takes an ``argv``-style list\nof alternating arg names and values and is responsible for validating\nthe correctness of argument names and values and returing a tuple with\ntwo members. The first member is a boolean ``is_valid``. The second is\nthe parsed list of argument values (without the argument names).\n\nHere is an example from the ``sofine`` plugin\n``sofine.plugins.standard.file_source``.\n\n::\n\n def parse_args(self, argv):\n \"\"\"`[-p|--path]` - Path to the file listing the keys to load into this data source.\"\"\"\n\n usage = \"[-p|--path] - Path to the file listing the keys to load into this data source.\"\n parser = OptionParser(usage=usage)\n parser.add_option(\"-p\", \"--path\", \n action=\"store\", dest=\"path\",\n help=\"Path to the file listing the keys to load into this data source. Required.\") \n (opts, args) = parser.parse_args(argv)\n\n is_valid = True\n if not opts.path:\n print \"Invalid argument error.\"\n print \"Your args: path {0}\".format(opts.path)\n print usage\n is_valid = False\n\n return is_valid, [opts.path]\n\n``get_schema``\n^^^^^^^^^^^^^^\n\nThe third method is ``get_schema``. You will rarely need to implement\nthis. Any plugin that knows the set of attributes it can return for a\nkey doesn't need to implement ``get_schema`` and can rely on the\ndefault, which returns the set of attribute keys you define.\n\n``get_namespaced_schema``\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n``get_namespaced_schema`` returns the set of attribute keys you define\nin ``self.schema`` in a namespace qualified with the plugin group and\nname. For example, if our stock quote plugin mentioned above is named\n``get_quotes`` and it is in the ``trading`` group, the return value of\n``get_schema`` would be ``[\"trading::get_quotes::quote\"]``. You do not\nhave to implement this, whether or not you implemented ``get_schema``,\nbecause ``sofine`` provides it by wrapping ``get_schema``.\n\nA Complete Plugin Example\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThis is a small amount of overhead compared to writing one-off scripts\nfor the return on investment of being able to know where your plugins\nare, call them with standard syntax, and compose them with each other in\nany useful combination.\n\nHow small? Here is the Google Search API plugin that ships with\n``sofine``.\n\nIt starts with a helper function that you would have to write in any\none-off script to call the API.\n\n::\n\n import urllib\n import urllib2\n import json\n\n def query_google_search(k):\n url = 'http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q={0}'.format(urllib.quote(k))\n ret = urllib2.urlopen(url)\n ret = ret.read()\n ret = json.loads(ret)\n\n if ret: 2\n ret = {'results' : ret['responseData']['results']}\n else:\n ret = {'results' : []}\n\n return ret\n\nNow, here are the 11 additional lines of code you need to make your\nplugin run in ``sofine``.\n\n::\n\n from sofine.plugins import plugin_base as plugin_base\n\n class GoogleSearchResults(plugin_base.PluginBase):\n\n def __init__(self):\n super(GoogleSearchResults, self).__init__()\n self.name = 'google_search_results'\n self.group = 'example'\n self.schema = ['results']\n self.adds_keys = False\n\n def get_data(self, keys, args):\n return {k : query_google_search(k) for k in keys}\n\n plugin = GoogleSearchResults\n\nJust for fun, here is a second example. This shows you how easy it is to\nwrap existing Python API wrappers as ``sofine`` plugins.\n\n::\n\n from sofine.plugins import plugin_base as plugin_base\n import ystockquote\n\n class YStockQuoteLib(plugin_base.PluginBase):\n\n def __init__(self):\n super(YStockQuoteLib, self).__init__()\n self.name = 'ystockquotelib'\n self.group = 'example'\n self.schema = ['fifty_two_week_low', 'market_cap', 'price', 'short_ratio', \n 'volume','dividend_yield', 'avg_daily_volume', 'ebitda', \n 'change', 'dividend_per_share', 'stock_exchange', \n 'two_hundred_day_moving_avg', 'fifty_two_week_high', \n 'price_sales_ratio', 'price_earnings_growth_ratio',\n 'fifty_day_moving_avg', 'price_book_ratio', 'earnings_per_share', \n 'price_earnings_ratio', 'book_value']\n self.adds_keys = False\n \n def get_data(self, keys, args):\n return {ticker : ystockquote.get_all(ticker) for ticker in keys} \n\n plugin = YStockQuoteLib\n\nHow HTTP Data Retrieval Plugins Work and How to Write Them\n----------------------------------------------------------\n\nYou may also implement plugins as HTTP servers. In this case you can\nimplement your plugin in any language you want. You call HTTP server\nplugins the same way you call Python plugins, using either the CLI API\nor the REST API. ``sofine`` will dynamically construct the URL to call\nyour HTTP plugin using the following elements:\n\n- the value you set in the environment variable\n ``SOFINE_HTTP_PLUGIN_URL``\n- the value you pass for ``plugin_name``\n- the value you pass for ``plugin_group``\n\nFor example, assuming you have set ``SOFINE_HTTP_PLUGIN_URL`` to be\n``http://localhostthis``\\ sofine\\` call:\n\n::\n\n python $PYTHONPATH/sofine/runner.py '--SF-s google_search_results --SF-g example_http --SF-a get_data\n\nwill call a plugin at this URL:\n\n::\n\n http://localhost/google_search_results/example_http/get_data\n\nHTTP Plugin Routes\n~~~~~~~~~~~~~~~~~~\n\nHTTP plugins have four routes, mapping to the methods in Python plugins.\n\n``/get_data``\n^^^^^^^^^^^^^\n\n``/get_data`` takes a list of keys and a list of arguments. Arguments\nare passed in the query string of the route call as query string\nparameter ``keys`` and ``args`` and so must be retrieved in the\nimplementation of your route.\n\nYour route handler must return a dict whose keys are a proper superset\nof the keys it received (the return set of keys can have more keys than\nwere passed to ``get_data`` if the plugin adds keys). This dict must\nhave string keys and a dict value for each key. The dict value is the\ndata retrieved for each key. The keys in that dict must be a set of\nstrings that is a proper subset of the set of strings in\n``self.schema``.\n\n``sofine`` ships with an example HTTP plugin written in ruby. It\nreimplements the Python example plugin that calls the Google Search\nResults API. Here is the HTTP plugin ``get_data`` route and handler:\n\n::\n\n get '/' + PLUGIN_NAME + '/' + PLUGIN_GROUP + '/get_data' do\n keys = params['keys'].split(',')\n ret = Hash[keys.map {|key| [key, query_google_search(key)]}] \n JSON.dump(ret)\n end\n\nYour HTTP plugin must implement this route.\n\nThe call to ``sofine`` to ``get_data`` results in call like this under\nthe hood.\n\n::\n\n 127.0.0.1 - - [06/Oct/2014 23:55:16] \"GET /google_search_results/example_http/get_data?keys=AAPL,MSFT&args= HTTP/1.1\" 200 4648 0.1324\n\n``/get_namespaced_data``\n^^^^^^^^^^^^^^^^^^^^^^^^\n\nA wrapper around ``get_data`` which returns the same data with attribute\nkeys wrapped in a namespace of the plugin group and name. This route is\noptional.\n\n``/parse_args``\n^^^^^^^^^^^^^^^\n\nIf your ``get_data`` requires no arguments you need not implement\n``parse_args``. But if your ``get_data`` call requires arguments, you\nmust implement ``parse_args``. The method takes an ``argv``-style list\nof alternating arg names and values in the query string parameter\n``args``, and is responsible for validating the correctness of argument\nnames and values and returing a tuple with two members. The first member\nis a boolean ``is_valid``. The second is the parsed list of argument\nvalues (without the argument names).\n\nHere is (somewhat trivial) example from the same example HTTP plugin,\n``google_search_results.rb``.\n\n::\n\n get '/' + PLUGIN_NAME + '/' + PLUGIN_GROUP + '/parse_args' do\n JSON.dump({\"parsed_args\" => params['args'], \"is_valid\" => true})\n end\n \n\n``/get_schema``\n^^^^^^^^^^^^^^^\n\nYou only need to implement this if your plugin doesn't know which\nattributes it returns whencalled. For example the\n``standard/file_source.py`` plugin that is part of ``sofine`` loads an\naaribtrary set of keys from a flat file and so can't know what data it\nmight return.\n\nIt returns a JSON object with a single key ``schema``. This key's value\nis the structure of the JSON returned by the route.\n\nAn example from the same Google plugin:\n\n::\n\n get '/' + PLUGIN_NAME + '/' + PLUGIN_GROUP + '/get_schema' do\n '{\"schema\" : [\"results\"]}'\n end\n\n``get_namespaced_schema``\n^^^^^^^^^^^^^^^^^^^^^^^^^\n\n``get_namespaced_schema and names`` returns the set of attribute keys in\na namespace qualified with the plugin group and name.\n\nAn example:\n\n::\n\n get '/' + PLUGIN_NAME + '/' + PLUGIN_GROUP + '/get_schema' do\n '{\"schema\" : [\"example_http::google_search_results::results\"]}'\n end\n\n``adds_keys``\n^^^^^^^^^^^^^\n\n``adds_keys`` lets users ask your plugin if it adds keys to the data set\nbeing built when ``sofine`` calls it, or if it just adds attributes for\nthe keys it receives.\n\nIt returns a JSON object with a single key, ``adds_keys``, which takes a\nboolean value indicating whether or not the plugin adds keys when\ncalled.\n\nHere is an example from the Google plugin:\n\n::\n\n get '/' + PLUGIN_NAME + '/' + PLUGIN_GROUP + '/adds_keys' do\n '{\"adds_keys\" : false}'\n end\n\nHow Data Format Plugins Work and How to Write Them\n--------------------------------------------------\n\n``sofine`` defaults to expecting input and returning output in JSON\nformat. The library also includes a CSV data format plugin. If these\ndon't meet your needs you can write your own, deploy them in your\n``SOFINE_DATA_FORMAT_PLUGIN_PATH`` plugin directory and the use them by\npassing an additional data format argument in your calls.\n\n- ``deserialize(data)`` - converts data in the data format to a Python\n data structure\n- ``serialize(data)`` - converts a Python data structure to the data\n format\n- ``get_content_type()`` - returns the correct value for the HTTP\n Content-Type header for the data format\n\nThe included ``format_json`` plugin provides a trivial example:\n\n::\n\n import json\n\n def deserialize(data):\n return json.loads(data)\n\n def serialize(data):\n return json.dumps(data)\n\n def get_content_type():\n return 'application/json'\n\nFormats without an isomorphic mapping to Python dicts and lists (which\ncorrespond to JSON objects and arrays) require some implementation.\nSpecifically, your plugin needs to be aware of the ``sofine`` data\nstructure for its data retrieval data sets, so that it can convert from\nthe data format into that Python data structure in ``deserialize`` anc\nconvert from that Python data structure into your data format (in a way\nthat makes sense and is documented in your plugin) in ``serialize``.\n\nRemember, ``sofine`` data sets look like this:\n\n::\n\n {\n \"AAPL\": \n [\n {\n \"results\": [\n {\n \"GsearchResultClass\": \"GwebSearch\",\n ...\n\n },\n ...\n ]\n },\n {\"avg_daily_volume\": \"59390100\"},\n {\"book_value\": \"20.193\"},\n ...\n ]\n } \n\nAs an example, here are the two methods in the included ``format_csv``\nplugin:\n\n::\n\n def deserialize(data):\n ret = {}\n schema = []\n\n reader = csv.reader(data.split(lineterminator), delimiter=delimiter, i\n lineterminator='', quoting=quoting, quotechar=quotechar)\n\n for row in reader:\n if not len(row):\n continue\n\n # 0th elem in CSV row is data row key\n key = row[0]\n key.encode('utf-8')\n \n attr_row = row[1:]\n ret[key] = [{attr_row[j].encode('utf-8') : attr_row[j + 1].encode('utf-8')}\n for j in range(0, len(attr_row) - 1, 2)]\n\n return ret\n\n\n def serialize(data):\n out_strm = BytesIO()\n writer = csv.writer(out_strm, delimiter=delimiter, lineterminator='|',\n quoting=quoting, quotechar=quotechar)\n\n # Flatten each key -> [attrs] 'row' in data into a CSV row with\n # key in the 0th position, and the attr values in an array in fields 1 .. N\n for key, attrs in data.iteritems():\n row = []\n row.append(key)\n for attr in attrs:\n row.append(attr.keys()[0])\n row.append(attr.values()[0])\n writer.writerow(row)\n\n ret = out_strm.getvalue()\n out_strm.close()\n\n return ret\n\nData Formats of Included Data Format Plugins\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nformat\\_json\n^^^^^^^^^^^^\n\nThe ``format_json`` plugin is isomorphic to the internal ``sofine`` data\nformat. Input data is in JSON that maps string keys to array of objects,\nwith each object having one string key and one string value. The keys\nare sofine data set keys; the array of objects is the array of key/value\nattributes associted with that key.\n\nSo the JSON input and output is in this format:\n\n::\n\n {\n \"AAPL\": \n [\n {\"avg_daily_volume\": \"59390100\"},\n {\"book_value\": \"20.193\"},\n ...\n ]\n } \n\nformat\\_csv\n^^^^^^^^^^^\n\nCSV data is not hierarchical, so ``sofine`` must make some design\ndecision about how to represent its data format in CSV. The library\nexpects input and output in CSV to be structured so that the key for\neach record is in the first field in a row, and the attribute keys and\nvalues mapped to that key follow on the same row with keys and values\nalternating. Essentially, each ``sofine`` record is just flattened into\na CSV row.\n\nUsing the same example:\n\n::\n\n AAPL, avg_daily_volume, 59390100, book_value, 20.193\n\nformat\\_xml\n^^^^^^^^^^^\n\nThe XML format attempts to map the JSON hierarchical data format of\n``sofine`` onto a reasonable XML representation.XML input and output\nlooks like this, for the same example:\n\n::\n\n \n \n AAPL\n \n \n avg_daily_volume\n 59390100\n \n \n book_value\n 20.193\n \n ...\n ...\n \n \n ...\n ...\n \n\nHow to Call Data Retrieval Plugins\n----------------------------------\n\nAs we saw above in the Introduction section, there are three ways to\ncall plugins, from the command line, as REST resources, or in Python.\nWhen calling plugins to retrieve data, you need to pass three or four\narguments, ``data``, the plugin name, the plugin group and the plugin\naction.\n\nThere are six actions, which correspond to the five methods\n``get_data``, ``get_namespaced_data``, ``parse_args``, ``get_schema``\nand ``get_namespaced_schema``, while ``adds_keys`` returns the value of\nthe the plugin's ``self.adds_keys``.\n\n::\n\n get_data\n get_namespaced_data\n parse_args\n get_schema\n get_namespaced_schema\n adds_keys\n\nCalling From the Command Line\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nWhen calling data retrieval plugins, you can optionally pass this\nargumehnt to control the data\\_format ``sofine`` expects any input to be\nin and the data format for the returned data set. This arguement is\npassed once before any sofine data retrieval calls, and applies that\nformat to all of the data retrieval calls.\n\n- ``[--SF-d|--SF-data-format]`` - The data format for input to a data\n retrieval call and for the returned data set. Optional. Default is\n 'json'.\n\nYou then pass these arguments for each data retreival call:\n\n- ``[--SF-s|--SF-data-source]`` - The name of the data source being\n called. This is the name of the plugin module being called. Required.\n- ``[--SF-g|--SF-data-source-group``] - The plugin group where the\n plugin lives. This is the plugins subdirectory where the plugin\n module is deployed. Required.\n- ``[--SF-a|--SF-action]`` - The plugin action being called. Optional\n if the action is ``get_data``.\n\nAny additional arguments that a call to ``get_data`` requires should be\npassed following the ``--SF-s`` and ``--SF-g`` arguments.\n\nCalling REST-fully\n~~~~~~~~~~~~~~~~~~\n\n``sofine`` ships with a server which you launch at\n``python sofine/rest_runner.py`` to call plugins over HTTP. The servers\nruns by default on ``localhost`` on port ``10000``. You can change the\nport it is running on by setting the environment variable\n``SOFINE_REST_PORT``. REST calls use the same arguments as CLI calls\nwithout the leading dashes. Args and their values alternate for form the\nresource path. See the examples in the following sections.\n\nget\\_data Examples\n~~~~~~~~~~~~~~~~~~\n\nHere are examples of calling ``get_data``:\n\n::\n\n python $PYTHONPATH/sofine/runner.py '--SF-s fidelity --SF-g example -c -p -a -e | --SF-s ystockquotelib --SF-g example'\n\nNotice that ``--SF-a`` is ommitted, which means this is chained call\nusing the default action ``get_data``, first from the ``fidelity``\nplugin (which is called first becasue it adds the set of keys returned)\nand then from the ``ystockquotelib`` plugin (which adds attributes to\nthe keys it received from ``fidelity``).\n\nIf you wanted to call this REST-fully, it would look nearly the same.\nThe syntax to chain calls is expressed by converting the sequence of\nargument names and values into a REST resource path.\n\n::\n\n curl -X POST -d '{}' --header \"Content-Type:application/json\" http://localhost:10000/SF-s/fidelity/SF-g/example/c//p//a//e//SF-s/ystockquotelib/SF-g/example\n\nHere is the same example from Python:\n\n::\n\n import sofine.runner as runner\n\n data = {}\n data_sources = ['fidelity', 'ystockquotelib']\n data_source_groups = ['example', 'example']\n data_source_args = [[customer_id, pin, account_id, email], []]\n data = runner.get_data_batch(data, data_sources, data_source_groups, data_source_args)\n\nThis call returns a data set of the form described above. Here is the\nJSON output:\n\n::\n\n {\n \"key_1\": [{\"attribute_1\": value_1}, {\"attribute_2\": value_2}, ...],\n \"key_2\": ...\n }\n\nget\\_data Example Using a Data Format Plugin\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nHere is the same call except using ``CSV`` instead of the default\n``JSON`` as the data format:\n\n::\n\n python $PYTHONPATH/sofine/runner.py '--SF-d format_csv --SF-s fidelity --SF-g example -c -p -a -e | --SF-s ystockquotelib --SF-g example'\n\nOther Actions\n~~~~~~~~~~~~~\n\nFinally, let's discuss the other actions besides ``get_data``. Note that\nnone of these actions can be chained.\n\nget\\_namespaced\\_data\n~~~~~~~~~~~~~~~~~~~~~\n\nWorks identically to ``get_data`` but you must included the ``--SF-a``\nargument in CLI calls or the ``SF-a`` argument in REST calls.\n\n::\n\n python $PYTHONPATH/sofine/runner.py '--SF-s fidelity --SF-g example --SF-a get_namespaced_data -c -p -a -e | --SF-s ystockquotelib --SF-g example --SF-a get_namespaced_data'\n\n curl -X POST -d '{}' --header \"Content-Type:application/json\" http://localhost:10000/SF-s/fidelity/SF-g/example/SF-a/get_namespaced_data/c//p//a//e//SF-s/ystockquotelib/SF-g/example/SF-a/get_namespaced_data\n\nThis call returns a data set of the form described above. Here is the\nJSON output.\n\n::\n\n {\n \"key_1\": [{\"plugin_group::plugin_name::attribute_1\": value_1}, \n i {\"plugin_group::plugin_name::attribute_2\": value_2}, ...],\n \"key_2\": ...\n }\n\nget\\_data\\_batch\n~~~~~~~~~~~~~~~~\n\nThis is a helper action only available within Python, to support\ncombining plugin calls into one batch call that returns one data set,\nequivalent to chaining command line or REST plugins in one call.\n\n::\n\n import sofine.runner as runner\n\n data = {}\n data_sources = ['fidelity', 'ystockquotelib']\n data_source_groups = ['example', 'example']\n data_source_args = [[customer_id, pin, account_id, email], []]\n data = runner.get_data_batch(data, data_sources, data_source_groups, data_source_args)\n\nNotice that the function takes a list of plugin names, a list of plugin\ngroups, and a list of lists of args. Each of these must put\ncorresponding plugins, groups and args in sequence.\n\nparse\\_args\n~~~~~~~~~~~\n\nYou should rarely need to call a plugins ``parse_args`` directly. One\nuse case is to test whether the arguments you plan to pass to\n``get_data`` are valid -- you might want to do this before making a\nlong-running ``get_data`` call, for example.\n\nFrom the CLI:\n\n::\n\n python $PYTHONPATH/sofine/runner.py '--SF-s file_source --SF-g standard --SF-a parse_args -p \"./sofine/tests/fixtures/file_source_test_data.txt\"'\n\nFrom REST:\n\n::\n\n curl -X POST -d '{}' --header \"Content-Type:application/json\" http://localhost:10000/SF-s/file_source/SF-g/standard/SF-a/parse_args/p/.%2Fsofine%2Ftests%2Ffixtures%2Ffile_source_test_data.txt\n\nFrom Python:\n\n::\n\n def test_parse_args_file_source(self):\n data_source = 'file_source'\n data_source_group = 'standard'\n path = './sofine/tests/fixtures/file_source_test_data.txt'\n args = ['-p', path]\n actual = runner.parse_args(data_source, data_source_group, args)\n\n self.assertTrue(actual['is_valid'] and actual['parsed_args'] == [path])\n\nThis call returns the following JSON and only JSON output is supported\nfor this call:\n\n::\n\n {\"is_valid\": true|false, \"parsed_args\": [arg_1, arg_2, ...]}\n\nget\\_schema\n~~~~~~~~~~~\n\nThere are several use cases for calling ``get_schema``, particularly\nfrom Python. For example, you might want to retrieve the attribute keys\nfrom one or several plugins being called together, to filter or query\nthe returned data for a subset of all the attribute keys.\n\nCLI:\n\n::\n\n python $PYTHONPATH/sofine/runner.py '--SF-s ystockquotelib --SF-g example --SF-a get_schema'\n\nREST:\n\n::\n\n curl -X POST -d '{}' --header \"Content-Type:application/json\" http://localhost:10000/SF-s/ystockquotelib/SF-g/example/SF-a/get_schema\n\nPython:\n\n::\n\n data_source = 'ystockquotelib'\n data_source_group = 'example'\n schema = runner.get_schema(data_source, data_source_group)\n\nThis call returns the following JSON and only JSON output is supported\nfor this call:\n\n::\n\n {\"schema\": [attribute_key_name_1, attribute_key_name_2, ...]}\n\nget\\_namespaced\\_schema\n~~~~~~~~~~~~~~~~~~~~~~~\n\nWorks identically to ``get_schema`` but returns the schema fields in\nnamespaced form.\n\nCLI:\n\n::\n\n python $PYTHONPATH/sofine/runner.py '--SF-s ystockquotelib --SF-g example --SF-a get_namespaced_schema'\n\nREST:\n\n::\n\n curl -X POST -d '{}' --header \"Content-Type:application/json\" http://localhost:10000/SF-s/ystockquotelib/SF-g/example/SF-a/get_namespaced_schema\n\nPython:\n\n::\n\n data_source = 'ystockquotelib'\n data_source_group = 'example'\n schema = runner.get_namespaced_schema(data_source, data_source_group)\n\nThis call returns the following JSON and only JSON output is supported\nfor this call:\n\n::\n\n {\n \"schema\": [plugin_group::plugin_name::attribute_key_name_1, \n plugin_group::plugin_name::attribute_key_name_2, ...]\n }\n\nadds\\_keys\n~~~~~~~~~~\n\nThe ``adds_keys`` action lets you ask a plugin programmatically whether\nit adds keys to the data set being built by ``sofine``. Let's say you\nwant to know which steps in a sequence of call to ``sofine`` plugins add\nkeys and which keys they add.\n\n::\n\n for name, group in plugin_map:\n prev_keys = set(data.keys())\n data = runner.get_data(data, name, group, args_map[name])\n \n if runner.adds_keys(name, group):\n new_keys = set(data.keys()) - prev_keys\n logger.log(new_keys)\n\nHere are examples of calling ``adds_keys``\n\nCLI:\n\n::\n\n python $PYTHONPATH/sofine/runner.py '--SF-s ystockquotelib --SF-g example --SF-a adds_keys'\n\nREST:\n\n::\n\n curl -X POST -d '{}' --header \"Content-Type:application/json\" http://localhost:10000/SF-s/ystockquotelib/SF-g/example/SF-a/adds_keys\n\nPython:\n\n::\n\n data_source = 'ystockquotelib'\n data_source_group = 'example'\n adds_keys = runner.adds_keys(data_source, data_source_group)\n\nThis call returns the following JSON and only JSON output is supported\nfor this call:\n\n::\n\n {\"adds_keys\": true|false} \n\nAdditional Convenience Methods\n------------------------------\n\nPlugins called from Python also expose two convenience methods that let\nyou get a reference to the plugin's module or to the plugin's class.\n\nget\\_plugin\n~~~~~~~~~~~\n\nThe ``get_plugin`` action lets you get an instance of a plugin object in\nPython. This lets you access class-scope methods or instance attributes\ndirectly.\n\nPython:\n\n::\n\n data_source = 'google_search_results'\n data_source_group = 'example' \n plugin = runner.get_plugin(data_source, data_source_group)\n schema = plugin.schema\n\nget\\_plugin\\_module\n~~~~~~~~~~~~~~~~~~~\n\nThe ``get_plugin_module`` action lets you get an instance of a plugin\nmodule in Python. This lets you access module-scope methods or variables\ndirectly. For exmample, the Google Search Results module implements an\nadditional helper called ``get_child_schema`` that returns the list of\nattributes in each of the ``results`` JSON objects that it returns for\neach key passed to it. Because this is nested data, the more interesting\nattributes are one level down in the data returned, which the helper\ntells us about.\n\n::\n\n data_source = 'google_search_results'\n data_source_group = 'example' \n mod = runner.get_plugin_module(data_source, data_source_group)\n # The google plugin implements an additional helper method in the module that returns \n # the list of attributes in each 'results' object it returns mapped to each key \n child_shema = mod.get_child_schema()\n\nManaging Python Data Retrieval Plugins\n--------------------------------------\n\nManaging data retrieval plugins is very simple. Pick a directory from\nwhich you want to call your plugins. Define the environment variable\n``SOFINE_PLUGIN_PATH`` and assign to it the path to your plugin\ndirectory.\n\nPlugins themselves are just Python modules (or code files exposing the\nrequired HTTP endpoints in the cast of HTTP plugins) fulfilling the\nrequirements detailed in the section, \"How Plugins Work and How to Write\nThem.\"\n\nPlugins cannot be deployed at the root of your plugin directory. Instead\nyou must create one or more subdirectories and place plugins in them.\nAny plugin can live in any subdirectory. If you want, you can even place\na plugin in more than one plugin directory. The plugin module name must\nmatch the plugin's ``self.name`` attribute, and the plugin directory\nname must match the plugin's ``self.group`` attribute.\n\nThis approach means you can manage your plugin directory without any\ndependencies on ``sofine``. You can manage your plugins directory as\ntheir own code repo, and include unit tests or config files in the\nplugin directory, etc.\n\nManaging HTTP Data Retrieval Plugins\n------------------------------------\n\n``sofine`` requires only one configuration dependency, that you define\n``SOFINE_HTTP_PLUGIN_URL``. Of course at the time you call your plugin,\nit needs to be running at that URL. Beyone that, you can manage the\nsource code and deployment of HTTP plugins completely independently of\n``sofine``.\n\nManaging Data Format Plugins\n----------------------------\n\nPick a direcgory from which you want to call your plugins. Define the\nenvironment variable ``SOFINE_DATA_FORMAT_PLUGIN_PATH`` and assign it to\nthe path of your plugin directory.\n\nUnlike data retrieval plugins, data format plugins should be deployed\ndirectly in your plugin directory, not in a subdirectory.\n\nData format plugins are simply modules. By convention they should be\nnamed ``format_.py``, for example, ``format_json.py``. This\nis optional, but provides a standard way to avoid name clashes with\nbuilt-in or third-party modules named after a data format, such as the\nPython standard library ``json`` and ``csv`` modules.\n\nAppendix: The Data Retrieval Algorithm\n--------------------------------------\n\n- The returned data set (let's call it \"data\") is always a JSON object\n of string keys mapped to an array of zero or more object values,\n where each object is a single attribute key and attribute value pair.\n- On every call in a ``sofine`` chain, add any new keys returned to\n data, and add all key attribute data returned to that key in data.\n- All attributes mapped to a key are JSON objects which themselves\n consist of string keys mapped to legal JSON values.\n\nSo the result of a call to a ``sofine`` pipe is the union of all keys\nretrieved by all plugin calls, with each key mapped to the union of all\nattributes returned by all plugin calls for that key.\n\nDeveloping With the sofine Code Base\n------------------------------------\n\nAll of the above documentation covers the very common case of using\nsofine as a library to manage and call your own plugins.\n\nHowever, you might want to develop with ``sofine`` more directly.\nPerhaps you want to use pieces of the library for other purposes, or\nfork the library to add features, or even contribute!\n\nIn that case, you'll want the developer documentation:\nhttp://marksweiss.github.io/sofine/", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://packages.python.org/sofine", "keywords": "glueAPI data pipelines scraper webAPI", "license": "MIT", "maintainer": null, "maintainer_email": null, "name": "sofine", "package_url": "https://pypi.org/project/sofine/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/sofine/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://packages.python.org/sofine" }, "release_url": "https://pypi.org/project/sofine/0.2.4.2/", "requires_dist": null, "requires_python": null, "summary": "Lightweight framework for creating data-collection plugins and chaining together calls to them, from CLI, REST or Python", "version": "0.2.4.2" }, "last_serial": 1268269, "releases": { "0.1": [ { "comment_text": "", "digests": { "md5": "1ec754c4e931177ceb3b9af175d9d08a", "sha256": "4416ded9e88dc0fa51f68cf0395503bb700756a6576d518b8d177e1247bc119e" }, "downloads": -1, "filename": "sofine-0.1.tar.gz", "has_sig": false, "md5_digest": "1ec754c4e931177ceb3b9af175d9d08a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 177813, "upload_time": "2014-08-06T04:31:12", "url": "https://files.pythonhosted.org/packages/c1/ad/3cdf57bd562778abb71b03844546b8bcb31287f9763beeaaad7c4c3df8b2/sofine-0.1.tar.gz" }, { "comment_text": "", "digests": { "md5": "49dc550e9ce5ca70530d6e8d52bef360", "sha256": "316bd2ae2e03268bd8a0a4e3c8ac798eb1d7c316005dc1cbde32e6ad22fcefec" }, "downloads": -1, "filename": "sofine-0.1.zip", "has_sig": false, "md5_digest": "49dc550e9ce5ca70530d6e8d52bef360", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 216834, "upload_time": "2014-08-06T04:31:16", "url": "https://files.pythonhosted.org/packages/e1/4b/fb66f4c78a5dd3e56a8a4d2237b0912f9fd1e9d6123260cba045fa7238e6/sofine-0.1.zip" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "0028390180325dcf84b771f6a1e013fd", "sha256": "8794644d9e875993db5245c56465c95e041358341f59b2791b771a05d5fa0c92" }, "downloads": -1, "filename": "sofine-0.1.1.tar.gz", "has_sig": false, "md5_digest": "0028390180325dcf84b771f6a1e013fd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 182531, "upload_time": "2014-08-06T17:45:00", "url": "https://files.pythonhosted.org/packages/1d/30/7ba2fc04eb8babe62a6ea122e5baf98119dc35bcf2cc822dc830c937d084/sofine-0.1.1.tar.gz" }, { "comment_text": "", "digests": { "md5": "ff832a63608097513a89354941c63623", "sha256": "146b8b3319e707ad1226d896769951f82cbca387442a140d7724df5c2a5d397e" }, "downloads": -1, "filename": "sofine-0.1.1.zip", "has_sig": false, "md5_digest": "ff832a63608097513a89354941c63623", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 218305, "upload_time": "2014-08-06T17:45:02", "url": "https://files.pythonhosted.org/packages/31/25/f077a63d964fd301be283397fd3f0380e326eb756dc1774567362b136a5e/sofine-0.1.1.zip" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "c2e08b2d4752cfeb2995bacec1b09c83", "sha256": "f24170372dc671eb974a05d0a8ea969b9d78310953740b0e3712e8f9e794ebbc" }, "downloads": -1, "filename": "sofine-0.1.2.tar.gz", "has_sig": false, "md5_digest": "c2e08b2d4752cfeb2995bacec1b09c83", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 182527, "upload_time": "2014-08-06T17:55:55", "url": "https://files.pythonhosted.org/packages/2d/63/67634c5bb043a0d90e5d15cda32333b246bd881986651f994790cbe2ee50/sofine-0.1.2.tar.gz" }, { "comment_text": "", "digests": { "md5": "cedb47375c2d701da64004e8e0067afa", "sha256": "6591cc6fa247ba55d6b26af5920998cfbd2c8afa1548b7641b85fccf2a89cd36" }, "downloads": -1, "filename": "sofine-0.1.2.zip", "has_sig": false, "md5_digest": "cedb47375c2d701da64004e8e0067afa", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 218300, "upload_time": "2014-08-06T17:55:57", "url": "https://files.pythonhosted.org/packages/70/e1/9c44f1f1aa3196131d88f8432ad99c41a58badee22d80c801d3ab2145038/sofine-0.1.2.zip" } ], "0.1.3": [ { "comment_text": "", "digests": { "md5": "b705d28ad7f56b3dc75163064299b14c", "sha256": "c88c01148d8d1621c5be4dab94a856fdeaeaf1dbda52be939af23efe20fb3177" }, "downloads": -1, "filename": "sofine-0.1.3.tar.gz", "has_sig": false, "md5_digest": "b705d28ad7f56b3dc75163064299b14c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 182474, "upload_time": "2014-08-07T04:11:52", "url": "https://files.pythonhosted.org/packages/81/21/bd0ee36f5b010460dd99328a608c17e902e57782116513cf74d6d2810746/sofine-0.1.3.tar.gz" }, { "comment_text": "", "digests": { "md5": "5c6a9dc5f6c68718380b10f8d4ed36e9", "sha256": "8493e230566d2d8375185ac4b8f118974be12ce29052d0aea11a1ba248eaa56a" }, "downloads": -1, "filename": "sofine-0.1.3.zip", "has_sig": false, "md5_digest": "5c6a9dc5f6c68718380b10f8d4ed36e9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 218235, "upload_time": "2014-08-07T04:11:55", "url": "https://files.pythonhosted.org/packages/4d/45/c3c8dbfa2bb28eafbfcc3776c9d24acad6df0eef1bec31b913a5ae43b7ec/sofine-0.1.3.zip" } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "d72874fea7612226381470a1aa0eaf68", "sha256": "d3c8939aa2973647769639db88614245b6a39f517c6df7da7a3c225191e91063" }, "downloads": -1, "filename": "sofine-0.2.1.tar.gz", "has_sig": false, "md5_digest": "d72874fea7612226381470a1aa0eaf68", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 194030, "upload_time": "2014-08-16T06:25:04", "url": "https://files.pythonhosted.org/packages/cc/80/7b4e9068011e646e46566e1272955c005941af8fdd50e412e12f25e4413b/sofine-0.2.1.tar.gz" }, { "comment_text": "", "digests": { "md5": "beed07ddcf8356aad7aa65eaacf673c4", "sha256": "9ee8a577fcb0f53cebed203c4f93855ebb193ad9fd20658d105826652f09f6e0" }, "downloads": -1, "filename": "sofine-0.2.1.zip", "has_sig": false, "md5_digest": "beed07ddcf8356aad7aa65eaacf673c4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 228573, "upload_time": "2014-08-16T06:25:06", "url": "https://files.pythonhosted.org/packages/df/d5/f91ff9bde556d13bfa6b43f1715b79a122105fdef67be5cef8fe9b6b068a/sofine-0.2.1.zip" } ], "0.2.2": [ { "comment_text": "", "digests": { "md5": "b76a8020b9fe207e4e2d46f16c124d31", "sha256": "6d2461e79fcea47d9e95a9fb0c65c3206d569d9e35b953598a1154ae769aec88" }, "downloads": -1, "filename": "sofine-0.2.2.tar.gz", "has_sig": false, "md5_digest": "b76a8020b9fe207e4e2d46f16c124d31", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 217413, "upload_time": "2014-08-25T07:58:33", "url": "https://files.pythonhosted.org/packages/62/68/8d01bf72bacd73e4cc0b719060366001c5da0282ed486db28124fe111b09/sofine-0.2.2.tar.gz" }, { "comment_text": "", "digests": { "md5": "d5da09feffd10d6b57cefaec84a2f4a1", "sha256": "8f8739467bdc80ee663cd55e27cdda609ade04c728bbbeee2bf6c007f43f8c64" }, "downloads": -1, "filename": "sofine-0.2.2.zip", "has_sig": false, "md5_digest": "d5da09feffd10d6b57cefaec84a2f4a1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 244912, "upload_time": "2014-08-25T07:58:36", "url": "https://files.pythonhosted.org/packages/fb/d4/dee2076c14751234f35bdbc471ef8effea1cba07ae45cfe97eefb1cb30a3/sofine-0.2.2.zip" } ], "0.2.3": [ { "comment_text": "", "digests": { "md5": "2bb18261b17df364f7306da598f3973f", "sha256": "2df26072b7847a39f8035644ddede7f5aa46a3262585fef65bc19d935e0816ed" }, "downloads": -1, "filename": "sofine-0.2.3.tar.gz", "has_sig": false, "md5_digest": "2bb18261b17df364f7306da598f3973f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 243563, "upload_time": "2014-09-11T06:20:46", "url": "https://files.pythonhosted.org/packages/bc/8f/35bd1f0a975f8d1c914cb8562562955b0d96c642912066f3d742b3db90fa/sofine-0.2.3.tar.gz" }, { "comment_text": "", "digests": { "md5": "b36a238749fd2845dcecf30bfcadafef", "sha256": "94dba74814748f3ba1bab7abfaba287667ef21204173bcbdd577303c895b86d4" }, "downloads": -1, "filename": "sofine-0.2.3.zip", "has_sig": false, "md5_digest": "b36a238749fd2845dcecf30bfcadafef", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 278566, "upload_time": "2014-09-11T06:20:49", "url": "https://files.pythonhosted.org/packages/62/b6/f7a3fa8aac6736fdc19462cdc6e9bb7449b7255cf4c4eb545bc2641e5579/sofine-0.2.3.zip" } ], "0.2.4": [ { "comment_text": "", "digests": { "md5": "3bc1156634a4eba7c4510cab32e15853", "sha256": "0f49c5f20d09393f2291576ef8a0a80216b0fec93c11e9c8876033f62862ac71" }, "downloads": -1, "filename": "sofine-0.2.4.tar.gz", "has_sig": false, "md5_digest": "3bc1156634a4eba7c4510cab32e15853", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 247425, "upload_time": "2014-10-13T06:05:55", "url": "https://files.pythonhosted.org/packages/b3/0f/158ec93d1dc2dd74d1509ebf42fb85cfd8bd162e9515ef45e22a5738209b/sofine-0.2.4.tar.gz" }, { "comment_text": "", "digests": { "md5": "ff793c59e86688710e17db0f905e888e", "sha256": "e35d52390fec15b49120d63e970ade4a42a69a30afe37e16467006297d009264" }, "downloads": -1, "filename": "sofine-0.2.4.zip", "has_sig": false, "md5_digest": "ff793c59e86688710e17db0f905e888e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 284672, "upload_time": "2014-10-13T06:05:58", "url": "https://files.pythonhosted.org/packages/7e/99/fe2c7df0878218ba2dc0626f855b256d168188af607eba304dec5fb02175/sofine-0.2.4.zip" } ], "0.2.4.1": [ { "comment_text": "", "digests": { "md5": "98f91c1542e624bba39a7ed4fe22f880", "sha256": "fac1b44240f180c6e9cadb0b2c5474ca33b96aeee54e4698f3e1b80d453d3863" }, "downloads": -1, "filename": "sofine-0.2.4.1.tar.gz", "has_sig": false, "md5_digest": "98f91c1542e624bba39a7ed4fe22f880", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 251636, "upload_time": "2014-10-13T06:12:11", "url": "https://files.pythonhosted.org/packages/19/9f/ad4d220dfd0cb60bf63fcff83805b0c8256f64532b94224837f4aa845f8f/sofine-0.2.4.1.tar.gz" }, { "comment_text": "", "digests": { "md5": "eca6f29ec8f7b929726dcaf62beb2b69", "sha256": "01d7e2afd863fc869e8be805ac5bf2440d8c7e615467912962a924b86aa3bd13" }, "downloads": -1, "filename": "sofine-0.2.4.1.zip", "has_sig": false, "md5_digest": "eca6f29ec8f7b929726dcaf62beb2b69", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 289088, "upload_time": "2014-10-13T06:12:14", "url": "https://files.pythonhosted.org/packages/02/a0/f10e97ec4875d5ada7206d94c12d4b2e100a08d840a417d8da8c45ce2065/sofine-0.2.4.1.zip" } ], "0.2.4.2": [ { "comment_text": "", "digests": { "md5": "ef85e5070661cdddc7ebe7f4533d7971", "sha256": "40127dc5d92c8e2ef2f9e74bd7b0fb8a017d1281bc73a7ee8362a6048b49aef8" }, "downloads": -1, "filename": "sofine-0.2.4.2.tar.gz", "has_sig": false, "md5_digest": "ef85e5070661cdddc7ebe7f4533d7971", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 260339, "upload_time": "2014-10-13T07:40:17", "url": "https://files.pythonhosted.org/packages/a8/ce/109fd04e4038875b907d22f8391c9b9a4547b8d7bab1ec09a0ce7495e419/sofine-0.2.4.2.tar.gz" }, { "comment_text": "", "digests": { "md5": "c85db752d6743612021243db9f3b1bfa", "sha256": "cc06e147fd06e75a0829c60a9ec976f0a3a732452b0f1cacbc9039d7f0af89a6" }, "downloads": -1, "filename": "sofine-0.2.4.2.zip", "has_sig": false, "md5_digest": "c85db752d6743612021243db9f3b1bfa", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 303376, "upload_time": "2014-10-13T07:40:20", "url": "https://files.pythonhosted.org/packages/57/3d/625b76dc5e2076766179047bfbf4c5735a1b502ee5fdbf4af0311d19a6f4/sofine-0.2.4.2.zip" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "ef85e5070661cdddc7ebe7f4533d7971", "sha256": "40127dc5d92c8e2ef2f9e74bd7b0fb8a017d1281bc73a7ee8362a6048b49aef8" }, "downloads": -1, "filename": "sofine-0.2.4.2.tar.gz", "has_sig": false, "md5_digest": "ef85e5070661cdddc7ebe7f4533d7971", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 260339, "upload_time": "2014-10-13T07:40:17", "url": "https://files.pythonhosted.org/packages/a8/ce/109fd04e4038875b907d22f8391c9b9a4547b8d7bab1ec09a0ce7495e419/sofine-0.2.4.2.tar.gz" }, { "comment_text": "", "digests": { "md5": "c85db752d6743612021243db9f3b1bfa", "sha256": "cc06e147fd06e75a0829c60a9ec976f0a3a732452b0f1cacbc9039d7f0af89a6" }, "downloads": -1, "filename": "sofine-0.2.4.2.zip", "has_sig": false, "md5_digest": "c85db752d6743612021243db9f3b1bfa", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 303376, "upload_time": "2014-10-13T07:40:20", "url": "https://files.pythonhosted.org/packages/57/3d/625b76dc5e2076766179047bfbf4c5735a1b502ee5fdbf4af0311d19a6f4/sofine-0.2.4.2.zip" } ] }