{ "info": { "author": "Ludwig Kristoffersson", "author_email": "ludwig@kristoffersson.org", "bugtrack_url": null, "classifiers": [], "description": "# Saas - Screenshot as a service [](https://circleci.com/gh/nattvara/saas)\n\n[](https://asciinema.org/a/KwtTFXEi4EhdsDUTglcw7aH8q?autoplay=1)\n\n\n## Installation\n\n### Requirements\n\n#### FUSE\n\n__What is fuse?__ From the [FUSE wikipedia page](https://en.wikipedia.org/wiki/Filesystem_in_Userspace)\n\n> Filesystem in Userspace (FUSE) is a software interface for Unix-like computer operating systems that lets non-privileged users create their own file systems without editing kernel code. This is achieved by running file system code in user space while the FUSE module provides only a \"bridge\" to the actual kernel interfaces.\n\nFUSE is used to mount a synthetic filesystem to read back the photos taken of the url given to saas. The user-space filesystem is dynamically filled with files and directories by saas. FUSE makes a good choice for this component since this can be easily integrated into almost any workflow, read more about this in the [API section](#api).\n\n#### Elasticsearch\n\n[Elasticsearch](https://www.elastic.co/products/elasticsearch) is used as a storage backend for saas. Read more about the storage in the [storage section](#storage).\n\n#### ImageMagick\n\n[ImageMagick](https://www.imagemagick.org) is used for optimizing image files saved to disk. This is an optional dependency since it is only used when the `--optimize-storage` flag is used.\n\n### Linux\n\n__1. Install Elasticsearch__ [using docker](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html)\n\n```bash\nsudo docker pull docker.elastic.co/elasticsearch/elasticsearch:6.5.4\n```\n\n__2. Install Firefox and Geckodriver__\n\n```bash\nsudo apt-get install firefox\n\nwget https://github.com/mozilla/geckodriver/releases/download/v0.23.0/geckodriver-v0.23.0-linux64.tar.gz\ntar -xvzf geckodriver-v0.23.0-linux64.tar.gz\nchmod +x geckodriver\nsudo mv geckodriver /usr/bin/\n```\n\n__3. Install ImageMagick (optional)__\n\n```bash\nsudo apt-get install imagemagick\n```\n\n__4. Install saas__\n\n```bash\n# Make sure you have Python 3.7 installed!\npython --version\n# Python 3.7.2\n\npip install saas\n\nsaas --version\n# saas 1.2.1\n```\n\n### macOS\n\n__1. Install FUSE for macOS__\n\nEither from [official website (recommended)](https://osxfuse.github.io/) or using homebrew\n\n```bash\nbrew update\nbrew tap homebrew/cask\nbrew cask install osxfuse\n```\n\n__2. Install Elasticsearch__\n\n```bash\nbrew install elasticsearch\n```\n\n__3. Install Firefox and Geckodriver__\n\nEither from [official website](https://mozilla.org/firefox) or using homebrew\n\n```bash\nbrew cask install firefox\n```\n\n__4. Install Geckodriver__\n\n```bash\nbrew install geckodriver\n```\n\n__5. Install Python 3.7__\n\n```bash\nbrew install python3\npython3 --version\n# Python 3.7.2\n```\n\n__6. Install ImageMagick (optional)__\n\n```bash\nbrew install imagemagick\n```\n\n__7. Install saas__\n\n```bash\n# Make sure you have Python 3.7 installed!\npython3 --version\n# Python 3.7.2\n\npython3 -m pip install saas\n\nsaas --version\n# saas 1.2.1\n```\n\n## Usage\n\n### Getting started\n\n#### Start Elasticsearch\n\nEverytime you run saas you must make sure that there is an elasticsearch instance running and availible is availible for saas to connect to.\n\n##### If using docker\n\n```bash\nsudo docker run -d -p 9200:9200 -e \"discovery.type=single-node\" docker.elastic.co/elasticsearch/elasticsearch:6.5.4\n```\n\n##### If binary exists in PATH\n\n```bash\n# foreground\nelasticsearch\n\n# or run it in the background\nelasticsearch 2>&1 > elasticsearch.log &\n```\n\n#### Taking a picture of a single URL\n\n```console\n\n# create input file\n$ touch input_urls\n\n# make mountpoint for filesystem\n$ mkdir mount\n\n# start saas\n# the --ignore-found-urls option will disable the crawler behaviour\n$ saas input_urls mount --ignore-found-urls\nmounting filesystem at: ./mount\nstarting 1 crawler threads\nstarting 1 photographer threads\n\n# add url to input file\n$ echo \"https://news.ycombinator.com/\" >> input_urls\n\n# the photo will appear inside the mountpoint\n$ tree mount/\nmount/\n\u2514\u2500\u2500 news.ycombinator.com\n \u251c\u2500\u2500 2019011721\n \u2502\u00a0\u00a0 \u2514\u2500\u2500 index.png\n \u2514\u2500\u2500 latest\n \u2514\u2500\u2500 index.png\n\n3 directories, 2 files\n```\n\n### Using the crawler\n\nThe crawler is a useful tool to find new urls to take pictuers of. It can be configured to *run wild* and crawl any domain it comes across, or stay at the domains that the urls in the input file belongs to.\n\n#### Stay at domains\n\nUsing the `--stay-at-domain` flag the crawler will discard any domain that does not belong to the same domain as the page it was found at.\n\n```console\n\n$ saas input_urls mount --stay-at-domain\n\n$ echo \"https://daringfireball.net/\" >> input_urls\n\n# after a minute or so\n\n$ tree mount/daringfireball.net/latest/\nmount/daringfireball.net/latest/\n\u251c\u2500\u2500 2006\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 06\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 apple_open_source.png\n\u251c\u2500\u2500 2007\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 01\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 enderle_leg_pulling.png\n\u251c\u2500\u2500 2008\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 04\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 big_fan.png.rendering.saas\n\u251c\u2500\u2500 2017\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 07\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 you_should_not_force_quit_apps.png\n\u251c\u2500\u2500 2019\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 01\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 on_getting_started_with_regular_expressions.png\n\u251c\u2500\u2500 index.png\n\u2514\u2500\u2500 linked\n \u2514\u2500\u2500 2019\n \u2514\u2500\u2500 01\n \u2514\u2500\u2500 07\n \u2514\u2500\u2500 samsung-itunes.png\n\n14 directories, 7 files\n```\n\n### Resetting the data\n\nSince the mounted filesystem is a read-only filesystem simply removing the a photo from the filesystem is currently not possible.\n\nFor now, at least, the best way to clear the data directory and the index is by using the `--clear-data-dir` and `--clear-elasticsearch` options\n\n```console\n# cannot modify the mounted filesystem\n$ touch mount/foo\ntouch: mount/foo: Read-only file system\n\n# clear the index of urls and photo metadata\n$ saas input_urls mount --clear-elasticsearch\n\n# clear the photo files\n$ saas input_urls mount --clear-data-dir\n```\n\n[Read more about storage](#storage)\n\n### Setting the viewport size\n\nThe camera viewport can be adjusted with the `--viewport-width` and `--viewport-height` options.\n\nBy default the camera tries to take a full screen screenshot. This means that it figures out how tall a page is and resizes the camera height accordingly. Full screen screenshots take way longer time, especially on image-heavy sites.\n\n### Full list of options\n\n```\nusage: saas [-h] [--version] [--debug] [--refresh-rate] [--crawler-threads]\n [--photographer-threads] [--data-dir] [--clear-data-dir]\n [--elasticsearch-host] [--setup-elasticsearch]\n [--clear-elasticsearch] [--stay-at-domain] [--ignore-found-urls]\n [--viewport-width] [--viewport-height] [--viewport-max-height]\n [--optimize-storage] [--stop-if-idle]\n url_file mountpoint\n\nScreenshot as a service\n\npositional arguments:\n url_file Path to input url file\n mountpoint Where to mount filesystem via FUSE\n\noptional arguments:\n -h, --help show this help message and exit\n --version show program's version number and exit\n --debug Display debugging information\n --refresh-rate Refresh captures of urls every 'day', 'hour' or\n 'minute' (default: hour)\n --crawler-threads Number of crawler threads, usually not neccessary with\n more than one (default: 1)\n --photographer-threads\n Number of photographer threads, beaware that\n increasing too much won't neccessarily speed up\n performance and hog the system (default: 1)\n --data-dir Path to data directory (default: ~/.saas-data-dir)\n --clear-data-dir Use flag to clear data directory on start\n --elasticsearch-host\n Elasticsearch host (default: localhost:9200)\n --setup-elasticsearch\n Use flag to create indices in elasticsearch\n --clear-elasticsearch\n Use flag to clear elasticsearch on start, WARNING:\n this will clear all indices found in elasticsearch\n instance\n --stay-at-domain Use flag to ignore urls from a different domain than\n the one it was found at\n --ignore-found-urls Use flag to ignore urls found on crawled urls\n --viewport-width Width of camera viewport in pixels (default: 1920)\n --viewport-height Height of camera viewport in pixels, if set to 0\n camera will try to take a full height high quality\n screenshot, which is way slower than fixed size\n (default: 0)\n --viewport-max-height\n Max height of camera viewport in pixels, if\n --viewport-height is set this will be ignored\n --optimize-storage Image files should be optimized to take up less\n storage (takes longer time to render)\n --stop-if-idle If greater than 0 saas will stop if it is idle for\n more than the provided number of minutes\n```\n\n
\n\n## Storage\n\nSaas uses two types of storages. A regular directory for storage of photo files, and elasticsearch for photo metadata and urls.\n\n### Elasticsearch\n\nThe elastic search instance is configured by saas with three indices\n\n - `crawled` this index holds urls that crawler have visited, the HTTP response code and any locks (meaning any photographer thread is taking a picture of that url)\n - `uncrawled` this index contains scraped urls from pages crawler have visited\n - `photos` this index contains photo metadata, file size, captured_at, filename etc.\n\n### Data directory\n\nWhen saas responds to a directory listing it only needs to query the elasticsearch `photos` index. Only when a read request is made, the actual file content is fetched from the data directory. The data directory holds the raw photo data with a unique id for each photo. Default path for this directory is `~/.saas-data-dir`\n\n```console\n$ tree ~/.saas-data-dir/\n\u251c\u2500\u2500 18\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 18dfe716-cdb2-4916-8154-6088d9bc6ee3.png\n\u251c\u2500\u2500 1c\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 1c1d0ee8-28f6-4b7c-b70f-8e800c58a3a6.png\n\u251c\u2500\u2500 29\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 29dd23f3-1791-46e6-8a83-25f5736a0894.png\n\u251c\u2500\u2500 50\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 50f13985-2cce-4464-942d-d9bbea165785.png\n\u251c\u2500\u2500 76\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 769933ce-2cde-4f30-a215-c26227850c8b.png\n\u251c\u2500\u2500 89\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 8975f15c-7112-499c-97d5-44dd501b9b09.png\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 89ec9675-84f8-47fa-9589-8d39a8a34ea1.png\n\u251c\u2500\u2500 ab\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 ab5bbb0f-03cb-45ed-be1d-e257434a925c.png\n\u251c\u2500\u2500 ca\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 ca1551a2-8855-4d0d-869b-108b9b7122bf.png\n\u2514\u2500\u2500 d7\n \u2514\u2500\u2500 d79598a2-619f-4192-bb39-5e31642be800.png\n```\n\n## Build\n\nInstall saas by cloning it from source\n\n```console\n$ git clone https://github.com/nattvara/saas.git && cd saas\n\n$ python3 -m venv ./venv\n\n$ source ./venv/bin/activate\n\n$ python setup.py develop\n```\n\n### Firefox extensions\n\nThe camera module uses selenium to render pages. To improve performance saas uses [uBlock Origin](https://github.com/gorhill/uBlock) to block ads. To have greater access to more webpages saas uses [I don't care about cookies](https://www.i-dont-care-about-cookies.eu/) to bypass popups and GDPR consent forms. Many websites also employ the practice of paywalls for some of their content, however, many websites leave their site open to users coming from search engines and social media sites. Saas therefore has a small custom [firefox extension](extensions/referer_header) to rewrite all http requests made from firefox to include the header `Referer: https://google.com` - this will allow access to a lot more content on the web.\n\n#### Updating uBlock Origin\n\nDownload the latest ublock.xpi from [gorhill/uBlock releases](https://github.com/gorhill/uBlock/releases) and replace the version in the `extensions/` directory.\n\n#### Updating IDCAC\n\nDownload and install the latest version using firefox from [https://www.i-dont-care-about-cookies.eu/](https://www.i-dont-care-about-cookies.eu/). Locate the `.xpi` file inside Firefox's extensions directory, on macOS this is `~/Library/Application Support/Firefox/Profiles/[profile]/extensions/`. Copy the `.xpi` file to the `extensions/` directory.\n\n#### Referer Header\n\nMake zip archive of source files\n\n```bash\nzip -r -j -FS extensions/referer_header.xpi extensions/referer_header/*\n```\n\n### Run the testsuite\n\n```console\n$ python -m unittest discover -s tests\n```\n\n### Run the typechecker\n\n```console\n$ mypy saas\n```\n\n\n\n## API\n\nThe main reason for using FUSE is that saas's api _is the filesystem_. Everything that can interact with the filesystem can interact with saas. Almost every programming language ships with easy access to the filesystem, hence integration in any environment is as easy as reading and writing to the filesystem.\n\nFor example exposing saas through a http interface could be as easy as starting a super simple node service like the following (should definitely be more thorough than this in production).\n\n```js\nconst http = require('http')\nconst url = require('url')\nconst fs = require('fs');\nconst port = 3000\n\nconst requestHandler = (request, response) => {\n fs.appendFile(\n 'urls',\n url.parse(request.url, true).query.url + '\\n',\n () => {}\n )\n response.end('')\n}\n\nconst server = http.createServer(requestHandler)\nserver.listen(port, (err) => {})\n```\n\nThis would allow for adding new urls to crawl by calling the service like the following\n\n```bash\ncurl http://localhost:3000/?url=https%3A%2F%2Fwww.wsj.com%2F\ncurl http://localhost:3000/?url=https%3A%2F%2Fwww.nytimes.com%2F\n```\n\nStarting a simple python webserver could allow for traversing the saas filesystem\n\n```bash\n# inside mounted filesystem\npython -m SimpleHTTPServer 3001\n\n# so the following url\n# https://www.ft.com/content/180f3428-1923-11e9-b93e-f4351a53f1c3\n# if photographed, could be found at\nwget http://localhost:3001/www.ft.com/latest/content/180f3428-1923-11e9-b93e-f4351a53f1c3.png\n```\n\nThose are two out of a hundred ways to integrate/extend saas.\n\n## Performance and Scalability\n\nSaas is designed to run over multiple machines. There can be virtually unlimited number of saas-nodes added to a single cluster, the only two things they need is a common elasticsearch instance or cluster to talk to, and a common data directory. Elasticsearch is well known for its scalability and the data directory could for instance be a network drive they share, Amazon EFS or any other way to share a drive between machines.\n\nSince all nodes in a cluster share the same index and data directory they can all read the images the cluster as a whole produces. Nodes can also join and leave the cluster freely without incurring any long time data loss.\n\nThe biggest hit to performance are taking photos of image-heavy sites or using a large viewport size. Fixed viewport size is a good option for optimizing performance, there is virtually no upper limit to how large a website can be vertically. Screenshots of tabloid websites or sites with infinite-scroll can easily reach 25-50 MB in size.\n\nCheckout the guide [Maximize saas throughput](docs/maximize_throughput_guide.md) for a thorough guide for how to deploy a large cluster of saas nodes on AWS and optimize performance.\n\n## Examples\n\nSee [examples/](examples/README.md) for some good examples for testing saas.\n\n## Known issues\n\nUnder some circumstances, a fatal crash for instance, the mounted filesystem might not unmount automatically. Also the filesystem will not be able to unmount if some other process is currently reading from the filesystem.\n\nIf you encouter this, run\n\n```bash\numount path/to/mounted_directory\n```\n\n## License\n\nMIT \u00a9 Ludwig Kristoffersson\n\nSee [LICENSE file](LICENSE) for more information\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/nattvara/saas", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "saas", "package_url": "https://pypi.org/project/saas/", "platform": "", "project_url": "https://pypi.org/project/saas/", "project_urls": { "Homepage": "https://github.com/nattvara/saas" }, "release_url": "https://pypi.org/project/saas/1.2.1/", "requires_dist": [ "wheel (==0.32.*)", "beeprint (==2.4.*)", "elasticsearch (==6.3.*)", "fusepy (==3.0.*)", "psutil (==4.3.*)", "selenium" ], "requires_python": ">=3.7", "summary": "Screenshot as a service", "version": "1.2.1" }, "last_serial": 5929043, "releases": { "1.1.0.1": [ { "comment_text": "", "digests": { "md5": "626b80f2c49f6d29025a5dc13e20fbf1", "sha256": "c86723104a252b68cd9218b55203e33efe106bfd96ad144841c69ea3da023ec9" }, "downloads": -1, "filename": "saas-1.1.0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "626b80f2c49f6d29025a5dc13e20fbf1", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 2775148, "upload_time": "2019-01-21T02:48:00", "url": "https://files.pythonhosted.org/packages/6a/e7/e1d2f3bb52095637cd531271834a44dc09b5f072fb1b1a401035793eb7f6/saas-1.1.0.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "356ed4e7f3830e97c1219cf725ff1740", "sha256": "683ff2f20b009bb430a177286f5acf61acbc33d4643b6682461ca03371970173" }, "downloads": -1, "filename": "saas-1.1.0.1.tar.gz", "has_sig": false, "md5_digest": "356ed4e7f3830e97c1219cf725ff1740", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 2770629, "upload_time": "2019-01-21T02:48:04", "url": "https://files.pythonhosted.org/packages/04/39/173dfec2446cd29c666916bb2f7f53b3f4881b51a60fc7c5b62aedba3c5e/saas-1.1.0.1.tar.gz" } ], "1.1.0.2": [ { "comment_text": "", "digests": { "md5": "92371313a24f8877993d44ca6b31a492", "sha256": "a3a70b118592077d69c99c68a1c00bbf418af844c4e0127c159b2bef62da3c3f" }, "downloads": -1, "filename": "saas-1.1.0.2-py3-none-any.whl", "has_sig": false, "md5_digest": "92371313a24f8877993d44ca6b31a492", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 2775108, "upload_time": "2019-01-22T18:28:01", "url": "https://files.pythonhosted.org/packages/4f/bb/51cbdbe78e42cedde57a5987f6d3cf1d547157dd4e05a1e70337864a4e77/saas-1.1.0.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "c82c838ca104ede1cbaa99bc79e58b95", "sha256": "f1a4b0df999029af18a730fc11adc15ba05861a528e855e4e5f600c7ff0cc080" }, "downloads": -1, "filename": "saas-1.1.0.2.tar.gz", "has_sig": false, "md5_digest": "c82c838ca104ede1cbaa99bc79e58b95", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 2770428, "upload_time": "2019-01-22T18:28:05", "url": "https://files.pythonhosted.org/packages/59/dc/1322440caaf31056c1a3bae533f976e0f8916a9003b75cb4f2f75ef58493/saas-1.1.0.2.tar.gz" } ], "1.1.3": [ { "comment_text": "", "digests": { "md5": "262325349224fdf28d8ac3ebc5c65c0c", "sha256": "a2534fc9c6eb4168507f1435520f19fe452e18e553fc779966bb14a4a766bb50" }, "downloads": -1, "filename": "saas-1.1.3-py3-none-any.whl", "has_sig": false, "md5_digest": "262325349224fdf28d8ac3ebc5c65c0c", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 5538909, "upload_time": "2019-04-02T16:00:59", "url": "https://files.pythonhosted.org/packages/94/90/7b5b1e6ea8c212ce46ecec79c2229aec650ad14fd5a2b709cf8d529114f7/saas-1.1.3-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "62e10c34370e2408d39dec9022a15ee6", "sha256": "c27034ecd217251b71bed7698ba23a771514a186cf363834eaa4c15be5680497" }, "downloads": -1, "filename": "saas-1.1.3.tar.gz", "has_sig": false, "md5_digest": "62e10c34370e2408d39dec9022a15ee6", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 2812865, "upload_time": "2019-04-02T16:01:03", "url": "https://files.pythonhosted.org/packages/06/b3/a7f4d2660a3857668e62a7bc85ffb142e31fabf7c127406c3b99e947acec/saas-1.1.3.tar.gz" } ], "1.1.3.1": [ { "comment_text": "", "digests": { "md5": "edb1f7dede4e5b09bc6b1009de4318dc", "sha256": "2824677a44ecc24ce147ddc16a32cd7eb2b7b64f47d6cfea31d9aebb572d1398" }, "downloads": -1, "filename": "saas-1.1.3.1-py3-none-any.whl", "has_sig": false, "md5_digest": "edb1f7dede4e5b09bc6b1009de4318dc", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 5539274, "upload_time": "2019-04-02T16:26:52", "url": "https://files.pythonhosted.org/packages/fa/0f/631d749202c2bae75d111e093c418e4aa393d9c92b537cb5878d707be701/saas-1.1.3.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f1b23c274cde3996f9397dfe2169e086", "sha256": "c9bfe0166b31b38585741e71c7218006e42727516b6e4b6259550ddbcb018b42" }, "downloads": -1, "filename": "saas-1.1.3.1.tar.gz", "has_sig": false, "md5_digest": "f1b23c274cde3996f9397dfe2169e086", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 2812927, "upload_time": "2019-04-02T16:26:54", "url": "https://files.pythonhosted.org/packages/b5/4d/db438665d5e199c1acf2b6e95aa392ded942ac27beaea1153dc05cfcb44f/saas-1.1.3.1.tar.gz" } ], "1.2": [ { "comment_text": "", "digests": { "md5": "1122ef80bb357985af9fb01808d68279", "sha256": "6da798eed7748b1258e46657c3b722974990cf44946bc66f5c73f7e2a65fd1dd" }, "downloads": -1, "filename": "saas-1.2-py3-none-any.whl", "has_sig": false, "md5_digest": "1122ef80bb357985af9fb01808d68279", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 2818559, "upload_time": "2019-04-28T18:05:57", "url": "https://files.pythonhosted.org/packages/ba/35/195c998c84d4534c8fee744b764f99b60a725cb4f9480af9fb3ec8753cd2/saas-1.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0168d601d960c3d9538613f0d455b991", "sha256": "1e024ba170edb831099bd1b19dca9a763b683b087ed34168f90efd974b7337b5" }, "downloads": -1, "filename": "saas-1.2.tar.gz", "has_sig": false, "md5_digest": "0168d601d960c3d9538613f0d455b991", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 2813593, "upload_time": "2019-04-28T18:07:09", "url": "https://files.pythonhosted.org/packages/da/ea/3e34dd1da1916fdfc2fdfd18965b9e45327539b03f0a8f8489e260949e85/saas-1.2.tar.gz" } ], "1.2.1": [ { "comment_text": "", "digests": { "md5": "0a2f9ec3b7e3d070a92d8f3d50749de9", "sha256": "a2c13163bdfd32a4f1f392c0195dfc9f88615b2ad98f97ef9da4fcc447dbfb59" }, "downloads": -1, "filename": "saas-1.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "0a2f9ec3b7e3d070a92d8f3d50749de9", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 2818191, "upload_time": "2019-10-04T15:47:03", "url": "https://files.pythonhosted.org/packages/88/53/8bcb9f70cc510fa4b63b3dfc94cd22f2768915847e001a6ce4883f5c694b/saas-1.2.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ced04fcdb9a30e819767dfd296bd6dff", "sha256": "db60780f7d382636d22b7e6c7ed136d23668246a6d8bfff68338d02886af545b" }, "downloads": -1, "filename": "saas-1.2.1.tar.gz", "has_sig": false, "md5_digest": "ced04fcdb9a30e819767dfd296bd6dff", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 2813254, "upload_time": "2019-10-04T15:47:05", "url": "https://files.pythonhosted.org/packages/ae/8c/9f94197387e7aa903d5529911bf40af9cd0286a013803459bab84eba4ed0/saas-1.2.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "0a2f9ec3b7e3d070a92d8f3d50749de9", "sha256": "a2c13163bdfd32a4f1f392c0195dfc9f88615b2ad98f97ef9da4fcc447dbfb59" }, "downloads": -1, "filename": "saas-1.2.1-py3-none-any.whl", "has_sig": false, "md5_digest": "0a2f9ec3b7e3d070a92d8f3d50749de9", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.7", "size": 2818191, "upload_time": "2019-10-04T15:47:03", "url": "https://files.pythonhosted.org/packages/88/53/8bcb9f70cc510fa4b63b3dfc94cd22f2768915847e001a6ce4883f5c694b/saas-1.2.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ced04fcdb9a30e819767dfd296bd6dff", "sha256": "db60780f7d382636d22b7e6c7ed136d23668246a6d8bfff68338d02886af545b" }, "downloads": -1, "filename": "saas-1.2.1.tar.gz", "has_sig": false, "md5_digest": "ced04fcdb9a30e819767dfd296bd6dff", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.7", "size": 2813254, "upload_time": "2019-10-04T15:47:05", "url": "https://files.pythonhosted.org/packages/ae/8c/9f94197387e7aa903d5529911bf40af9cd0286a013803459bab84eba4ed0/saas-1.2.1.tar.gz" } ] }