{ "info": { "author": "Climapulse NV", "author_email": "kevin.wetzels@climapulse.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Framework :: Django", "Framework :: Django :: 1.8", "Framework :: Django :: 1.9", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5" ], "description": "=============================\ndj-bgfiles\n=============================\n\n.. image:: https://badge.fury.io/py/dj-bgfiles.png\n :target: https://badge.fury.io/py/dj-bgfiles\n\n.. image:: https://travis-ci.org/climapulse/dj-bgfiles.png?branch=master\n :target: https://travis-ci.org/climapulse/dj-bgfiles\n\nGenerate files in the background and allow users to pick them up afterwards using a token.\n\nDocumentation\n-------------\n\nThe \"full\" documentation is at https://dj-bgfiles.readthedocs.org.\n\nQuickstart\n----------\n\nFirst ensure you're using Django 1.8.4 or up. Next, install dj-bgfiles::\n\n pip install dj-bgfiles\n\nAdd it your ``INSTALLED_APPS`` and make sure to run the ``migrate`` command to create the necessary tables.\n\n\nFeatures\n--------\n\nThe most common use case is delivering sizeable data exports or complex reports to users without overloading the web\nserver, which is why ``bgfiles`` provides tools to:\n\n- Store a file generation/download request\n- Process that request later on, e.g. by using Celery or a cron job\n- Send a notification to the requester including a secure link to download the file\n- And serving the file to the requester (or others if applicable)\n\nExample\n-------\n\nIn our hypothetical project we want to provide users with the option to generate an xlsx file containing login events. Here's our model::\n\n class LoginEvent(models.Model):\n user = models.ForeignKey(User, blank=True, null=True)\n succeeded = models.BooleanField(default=True)\n created_at = models.DateTimeField(auto_now_add=True)\n\n def save(self, *args, **kwargs):\n self.succeeded = True if self.user_id else False\n super(LoginEvent, self).save(*args, **kwargs)\n\nOur export allows a user to specify a timeframe through a form::\n\n class LoginExportFilterForm(models.Model):\n from_date = models.DateField()\n until_date = models.DateField(required=False)\n\n def apply_filters(self, queryset):\n \"\"\"Apply the filters to our initial queryset.\"\"\"\n data = self.cleaned_data\n queryset = queryset.filter(created_at__gte=data['from_date'])\n until_date = data.get('until_date')\n if until_date:\n queryset = queryset.filter(created_at__lte=until_date)\n return queryset\n\nSo our view would look like this initially::\n\n def export_login_events(request):\n if request.method == 'POST':\n form = LoginExportFilterForm(data=request.POST)\n if form.is_valid():\n # Grab our events\n events = LoginEvent.objects.all()\n # Apply the filters the user supplied\n events = form.apply_filters(events)\n # And write everything to an xlsx file and serve it as a HttpResponse\n return write_xlsx(events)\n else:\n form = LoginExportFilterForm()\n return render(request, 'reports/login_events_filter.html', context={'form': form})\n\nBut overnight, as it happens, our app got really popular so we have a ton of login events. We want to offload the\ncreation of large files to a background process that'll send a notification to the user when the file's ready with a\nlink to download it.\n\nEnter ``bgfiles``.\n\nManually, using a Celery task\n#############################\n\nHere's how we could handle this manually using our ``toolbox`` and a Celery task::\n\n from bgfiles import toolbox\n\n def export_login_events(request):\n if request.method == 'POST':\n form = LoginExportFilterForm(data=request.POST)\n if form.is_valid():\n # Grab our events\n events = LoginEvent.objects.all()\n # Apply the filters the user supplied\n events = form.apply_filters(events)\n # We want to limit online file creation to at most 10000 events\n max_nr_events = 10000\n nr_events = events.count()\n if nr_events <= max_nr_events:\n # Ok, deliver our file to the user\n return write_xlsx(events)\n # The file would be too big. So let's grab our criteria\n criteria = request.POST.urlencode()\n # The content type is optional\n content_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n # Now add a file request\n with transaction.atomic():\n file_request = toolbox.add_request(criteria, file_type='login_events',\n requester=request.user, content_type=content_type)\n # Schedule a Celery task\n export_login_events_task.delay(file_request.id)\n # And let the user know we'll get back to them on that\n context = {'nr_events': nr_events, 'max_nr_events': max_nr_events}\n return render(request, 'reports/delayed_response.html', context=context)\n\n else:\n form = LoginExportFilterForm()\n return render(request, 'reports/login_events_filter.html', context={'form': form})\n\nWhen we add a file request, we first marshall our criteria to something our database can store and we can easily\nunmarshall later on. We specify a file type to support requests for different types of files and also record the\nuser that requested the file. The default ``bgfiles`` logic assumes only the user performing the request should be\nable to download the file later on.\n\nAnyway, our Celery task to create the file in the background might look like this::\n\n from bgfiles.models import FileRequest\n from django.http import QueryDict\n\n @task\n def export_login_events_task(file_request_id):\n # Grab our request. You might want to lock it or check if it's already been processed in the meanwhile.\n request = FileRequest.objects.get(id=file_request_id)\n # Restore our criteria\n criteria = QueryDict(request.criteria)\n # Build our form and apply the filters as specified by the criteria\n form = LoginExportFilterForm(data=criteria)\n events = LoginEvent.objects.all()\n events = form.apply_filters(events)\n # Write the events to an xlsx buffer (simplified)\n contents = write_xlsx(events)\n # Attach the contents to the request and add a filename\n toolbox.attach_file(request, contents, filename='login_events.xlsx'):\n # Generate a token for the requester of the file\n token = toolbox.create_token(request)\n # Grab the download url including our token\n download_url = reverse('mydownloads:serve', kwargs={'token': token})\n # And send out an email containing the link\n notifications.send_file_ready(request.requester, download_url, request.filename)\n\nIt should be pretty obvious what the above code is doing. Note that restoring our criteria is easy: we simply\ninstantiate a QueryDict. Yes, there's a bit of code duplication. We'll get to that later on.\n\nManually, using a cron job\n##########################\n\nLet's defer the generation to a cron job that will send out an email to our user. Our view would look the same, except\nwe won't schedule a Celery task. Our cron logic then might look like this::\n\n from bgfiles import toolbox\n from bgfiles.models import FileRequest\n from django.http import QueryDict\n\n def process_file_requests():\n # Only grab the requests that still need to be processed\n requests = FileRequest.objects.to_handle()\n # Process each one by delegating to a specific handler\n for request in requests:\n if request.file_type == 'login_events':\n process_login_events(request)\n elif request.file_type == 'something_else:\n process_something_else(request)\n else:\n raise Exception('Unsupported file type %s' % request.file_type)\n\n def process_login_events(request):\n # Restore our criteria\n criteria = QueryDict(request.criteria)\n # Build our form and apply the filters as specified by the criteria\n form = LoginExportFilterForm(data=criteria)\n events = LoginEvent.objects.all()\n events = form.apply_filters(events)\n # Write the events to an xlsx buffer (simplified)\n contents = write_xlsx(events)\n # Attach the contents to the request and add a filename\n toolbox.attach_file(request, contents, filename='login_events.xlsx'):\n # Generate a token for the requester of the file\n token = toolbox.create_token(request)\n # Get the download url\n download_url = reverse('mydownloads:serve', kwargs={'token': token})\n # Send out an email containing the link\n notifications.send_file_ready(request.requester, download_url, request.filename)\n\nAdd a management command to call ``process_file_requests``, drop it in crontab and you're good to go.\n\nBut wait! There's more!\n\n\nUsing the FullPattern\n#####################\n\n``bgfiles`` includes common patterns to structure your logic and minimize the code duplication. As you can see above\ntheir usage is entirely optional.\n\nIn this example we'll use the ``bgfiles.patterns.FullPattern`` to render a template response when the file creation is\ndelayed and send out an email notification when the file is ready.\n\nHere's our export handler class::\n\n class LoginEventExport(FullPattern):\n # These can all be overridden by get_* methods, e.g. get_file_type\n file_type = 'login_events'\n content_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n delayed_template_name = 'reports/delayed_response.html'\n email_subject = _('File %(filename)s is ready!')\n email_text_message = _('Come and get it: %(download_url)s')\n\n def get_items(self, criteria):\n # Our default criteria class provides the request's QueryDict (e.g. request.POST) as `raw` and the\n # requester as `user`. This method is used for both online and offline selection of the items we want to\n # use.\n form = LoginExportFilterForm(data=criteria.raw)\n if not form.is_valid():\n # If the form is invalid we raise an exception so our view knows about it and can show the errors.\n # If the form became invalid while we're offline... well, that shouldn't happen.\n raise InvalidForm(form=form)\n # Valid form: apply our filters and return our events\n return form.apply_filters(LoginEvent.objects.all())\n\n def evaluate_dataset(self, dataset, http_request):\n # When we've got our dataset, including our criteria and items, we need to evaluate whether we can\n # deliver it right now or need to delay it. Let's use our magic number.\n # Note that this is only called during an HTTP request.\n dataset.delay = dataset.items.count() > 10000\n\n def write_bytes(self, dataset, buffer):\n # This is where we write our dataset to the buffer. What goes on in here depends on your dataset, type of\n # file and so on.\n\nNow let's adapt our view to use it::\n\n def export_login_events(request):\n if request.method == 'POST':\n try:\n delayed, response = LoginEventExport('login-events.xlsx').respond_to(request)\n return response\n except InvalidForm as exc:\n form = exc.form\n else:\n form = LoginExportFilterForm()\n return render(request, 'reports/login_events_filter.html', context={'form': form})\n\nHere's what happening:\n\n1. We let our export class handle the response when it's a POST request\n2. It builds our dataset by wrapping the criteria (so we can use the same thing for both online and offline file generation) and fetching the items using ``get_items`` based on those criteria\n3. It then lets you evaluate the dataset to decide on what to do next\n\nIf we don't delay the file creation, the pattern will write our bytes to a ``HttpResponse`` with the specified filename and content type.\n\nBut when we *do* delay the creation, it will:\n\n1. Add a ``bgfiles.models.FileRequest`` to the database\n2. Ask you to schedule the request using its ``schedule`` method\n3. Respond with a template response using the ``delayed_template_name``\n\nThe ``schedule`` method does nothing by default, but if you use the included management command you can still have a cron\njob process the outstanding requests automatically. If you prefer to use Celery, you can use the ``bgfiles.patterns.celery.ScheduleWithCeleryPattern``\nclass instead. It subclasses the ``FullPattern`` class.\n\nThis has our online part covered, but we still need to adapt our cron job. Here's what's left of it using the pattern::\n\n def process_login_events(request):\n LoginEventExport(request.filename).create_file(request)\n\n\nThat's it. The default implementation of ``create_file`` will:\n\n1. Restore our criteria using the ``criteria_class`` specified on our exporter\n2. Call ``get_items`` using those criteria\n3. Call ``write_bytes`` to generate the file contents\n4. Hook up the contents to our ``FileRequest`` and mark it as finished\n5. Send out an email notification to the requester\n\nBut we can still improve. Read on!\n\n\nUsing the management command\n############################\n\nThe included management command ``bgfiles`` allows you to clean up expired file requests, whether you use the included patterns or not::\n\n $ python manage.py bgfiles clean --timeout=60 --sleep=1\n\nThe above command will clean expired file requests, but will stop after a minute (or close enough) and go to sleep\nfor a second in between requests. By default it will also ignore file requests that have expired less than an\nhour ago as to not interrupt any ongoing last-minute downloads. You can override this using the ``--leeway`` parameter.\n\nYou can also use the management command to process outstanding requests. To do this you'll need to register your\nexporter in the registry::\n\n from bgfiles.patterns import FullPattern, registry\n\n class LoginEventExport(FullPattern):\n # Same as above\n\n\n # Register our class to process requests for our file type.\n # You might want to place this in your AppConfig.ready.\n registry.register(LoginEventExport, [LoginEventExport.file_type])\n\n\nNow all that's needed to process file requests is to call the management command::\n\n $ python manage.py bgfiles process --sleep=1 --timeout=600 --items=20\n\n\nThis will process our outstanding requests by looking up the correct handler in the registry and calling the handler's ``handle``\nclassmethod which by default will restore a class instance (using the pattern's ``restore`` classmethod) and call its\n``create_file`` method.\n\n\nServing the file\n################\n\nWe've included a view you can use to serve the file. It will verify the token is valid, not expired and, by default,\ncheck that the accessing user is also the user that requested the file. It doesn't provide any decent error messages\nin case something is wrong, so you might want to wrap it with your own view::\n\n from bgfiles.views import serve_file, SuspiciousToken, SignatureHasExpired, UserIsNotRequester\n\n def serve(request, token):\n try:\n return serve_file(request, token, require_requester=True, verify_requester=True)\n except SuspiciousToken:\n # The token is invalid.\n # You could reraise this so Django can warn you about this suspicious operation\n return render(request, 'errors/naughty.html')\n except SignatureHasExpired:\n # Actually a subclass of PermissionDenied.\n # But you might want to inform the user they're a bit late to the party.\n return render(request, 'errors/signature_expired.html')\n except UserIsNotRequester:\n # Also a PermissionDenied subclass.\n # So the user's email was intercepted or they forwarded the mail to someone else.\n # Set verify_requester to False to disable this behavior.\n return render(request, 'errors/access_denied.html')\n\n\nAllowing anyone to access a file\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThat is anyone with a valid token. By default ``bgfiles`` will assume you only want to serve a file to the user that\nrequested it. If you want to serve the file to anyone with a valid token it's just as easy.\n\nManually\n~~~~~~~~\n\nIn our manual example we used ``toolbox.create_token`` to create our token. This embeds the id of the requester in the\ntoken. To create a token anyone can use, use ``toolbox.create_general_token`` instead. Of course, the other token can\nalso be used by anyone because the verification is done in the view::\n\n # When we only want to serve the file to the requester, we use this:\n serve_file(request, token, require_requester=True, verify_requester=True)\n\n # When we want to serve the file to anyone, we use this:\n serve_file(request, token, require_requester=False, verify_requester=False)\n\nUsing patterns\n~~~~~~~~~~~~~~\nYou can tell the pattern to use \"general\" tokens by setting the ``requester_only`` class variable to ``False`` or\nby letting the ``is_requester_only`` method return ``False``. The changes to the view are the same as above.\n\n\nChanging the signer\n^^^^^^^^^^^^^^^^^^^\n\nBy default ``bgfiles`` uses Django's signing module to handle tokens. Configuring the signer can be done by changing settings:\n\n- ``BGFILES_DEFAULT_KEY``: defaults to ``SECRET_KEY``\n- ``BGFILES_DEFAULT_SALT``: defaults to ``bgfiles``. **We recommend specifying your own salt.**\n- ``BGFILES_DEFAULT_SERIALIZER``: defaults to the ``JSONSerializer`` class included in the toolbox\n- ``BGFILES_DEFAULT_MAX_AGE``: defaults to 86400 seconds (or a day)\n\nIf you need a custom default `Signer`, you can set ``BGFILES_DEFAULT_SIGNER`` to an instance of your signer class.\n\nOf course, you should think about when to use the default signing method and settings and when to deviate and use a\ncustom one. Just be sure to use the same signer configuration when creating a token and when accepting a token::\n\n token = toolbox.create_token(request, signer=my_signer)\n\n # And in your view\n def serve(request, token):\n try:\n return serve_file(request, token, require_requester=True, verify_requester=True, signer=my_signer)\n except Stuff:\n # Error handling goes here\n\nSpecifying a custom signer on a pattern class::\n\n class MyExporter(FullPattern):\n signer = my_signer\n\n\nOr::\n\n class MyExporter(FullPattern):\n\n def get_signer(self):\n return my_signer\n\n\n\n\n\nHistory\n-------\n\nHistory is not collected yet. This is alpha software.\n\n\n", "description_content_type": null, "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/climapulse/dj-bgfiles", "keywords": "dj-bgfiles,django", "license": "BSD", "maintainer": "", "maintainer_email": "", "name": "dj-bgfiles", "package_url": "https://pypi.org/project/dj-bgfiles/", "platform": "", "project_url": "https://pypi.org/project/dj-bgfiles/", "project_urls": { "Homepage": "https://github.com/climapulse/dj-bgfiles" }, "release_url": "https://pypi.org/project/dj-bgfiles/0.6.0/", "requires_dist": null, "requires_python": "", "summary": "Generate files in the background", "version": "0.6.0" }, "last_serial": 3552538, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "14a5b6153a0a69f815d32e83041b63d4", "sha256": "02dc56646fd61efdfcfb355d632556f2e91bb603b969f45c536302a7119f5267" }, "downloads": -1, "filename": "dj_bgfiles-0.1.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "14a5b6153a0a69f815d32e83041b63d4", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 21457, "upload_time": "2016-01-25T21:08:15", "url": "https://files.pythonhosted.org/packages/ef/c4/763b40ef1e004a0fd27e37b0af4560f4c80e06569abfd7a7558985bc7301/dj_bgfiles-0.1.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "90adf3aa5343c1fff664459bbc67cfc8", "sha256": "f645b78d044f723856caedec9c6acc302cd442bbd36ace4fc309d29d5117a5ea" }, "downloads": -1, "filename": "dj-bgfiles-0.1.0.tar.gz", "has_sig": false, "md5_digest": "90adf3aa5343c1fff664459bbc67cfc8", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 32499, "upload_time": "2016-01-25T21:08:24", "url": "https://files.pythonhosted.org/packages/24/13/018b4a5756d26223256558605b3b3493193e01efc9bd4d062a001477861f/dj-bgfiles-0.1.0.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "b8428aaff002911a9e7c8374461e920a", "sha256": "dfeecc80efaa662997aa8f858ad6b877ee8380e322ffb2df33d86931c3188905" }, "downloads": -1, "filename": "dj_bgfiles-0.2.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "b8428aaff002911a9e7c8374461e920a", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 31772, "upload_time": "2016-01-26T08:22:29", "url": "https://files.pythonhosted.org/packages/77/5c/2a6c1575a81cf68dfd8bed338e5db2db615459c378f60701d841cc037439/dj_bgfiles-0.2.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "83526061df4684a7c0deec258407c46d", "sha256": "a5e6919092528970719c9a49410da53cd628698b533ce812e99c85fdc5ed125e" }, "downloads": -1, "filename": "dj-bgfiles-0.2.0.tar.gz", "has_sig": false, "md5_digest": "83526061df4684a7c0deec258407c46d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42606, "upload_time": "2016-01-26T08:18:58", "url": "https://files.pythonhosted.org/packages/05/72/26879e1dba5ec86d5e1ec68b80657ed7a5515006c747a2ac06c4c730e157/dj-bgfiles-0.2.0.tar.gz" } ], "0.3.0": [ { "comment_text": "", "digests": { "md5": "9092cc070120ccd1436182f9d7a27890", "sha256": "ae78aeedfb858a3195ab69fec621adb2a3ac8679385adea456c9dbb5f41fba8b" }, "downloads": -1, "filename": "dj_bgfiles-0.3.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "9092cc070120ccd1436182f9d7a27890", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 32092, "upload_time": "2016-01-27T21:10:48", "url": "https://files.pythonhosted.org/packages/a5/7a/66c95fc904acfab2fd0928dcbba5cdc3e715e5fc87940d6fc72f364a109a/dj_bgfiles-0.3.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0f85bfef482f3b24aff51b852a1a2db4", "sha256": "df36804773f5fa7d43299f21224ee6fbab27ebb5b1f511b8c48df1e2e411e154" }, "downloads": -1, "filename": "dj-bgfiles-0.3.0.tar.gz", "has_sig": false, "md5_digest": "0f85bfef482f3b24aff51b852a1a2db4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42884, "upload_time": "2016-01-27T21:10:56", "url": "https://files.pythonhosted.org/packages/9b/57/a0dbf9f0bfed3acf9ab68463185bf9dd15894b30b8a9bd30bca7ab7ae8ae/dj-bgfiles-0.3.0.tar.gz" } ], "0.4.0": [ { "comment_text": "", "digests": { "md5": "0fdf3944ca1f67042716d01b05f1f386", "sha256": "5333574785529ae2acbee25e269df63d2585626489dae154edae1d2005aa41df" }, "downloads": -1, "filename": "dj_bgfiles-0.4.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "0fdf3944ca1f67042716d01b05f1f386", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 32720, "upload_time": "2016-02-04T08:31:44", "url": "https://files.pythonhosted.org/packages/d6/ff/3b959c5aafd4d47a2a37ee86a9d48a03c4f1f498bfc61eb5da4ca737ed0b/dj_bgfiles-0.4.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "df4ff38794673ae100c6e3e4f1f83d45", "sha256": "5ef732394508571e006f1d219c8d5c958763d735cf250c3a516ea143ee9dc8c3" }, "downloads": -1, "filename": "dj-bgfiles-0.4.0.tar.gz", "has_sig": false, "md5_digest": "df4ff38794673ae100c6e3e4f1f83d45", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 43198, "upload_time": "2016-02-04T08:31:50", "url": "https://files.pythonhosted.org/packages/7a/40/e19d8c4dd1fcd41068eefcec9b9641cbf82c42dfe311bf6cc866ff2afec1/dj-bgfiles-0.4.0.tar.gz" } ], "0.5.0": [ { "comment_text": "", "digests": { "md5": "a989ac1452ac4ef04b68029214c5c421", "sha256": "d1d16e2a42e460f40d7c8c64ae40285c3f21c1dc85e1b7070b2a5e6b1eca1665" }, "downloads": -1, "filename": "dj_bgfiles-0.5.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "a989ac1452ac4ef04b68029214c5c421", "packagetype": "bdist_wheel", "python_version": "2.7", "requires_python": null, "size": 32717, "upload_time": "2017-01-14T14:43:30", "url": "https://files.pythonhosted.org/packages/65/24/1c9dd0db7808df967e2b6426d30922ac6ad9ff0337483873b627ef7e4ff4/dj_bgfiles-0.5.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "8323e340672a9b612bac971177372932", "sha256": "ad3914aa1577865aa1853781c4f080452ca7e8ee9e1e09748116ef0ee8af988b" }, "downloads": -1, "filename": "dj-bgfiles-0.5.0.tar.gz", "has_sig": false, "md5_digest": "8323e340672a9b612bac971177372932", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 43212, "upload_time": "2017-01-14T14:43:24", "url": "https://files.pythonhosted.org/packages/ca/81/bcd81b1c23e4941b7b8ec87910a16b11cac3d2379074f1a718a0c441eb4a/dj-bgfiles-0.5.0.tar.gz" } ], "0.6.0": [ { "comment_text": "", "digests": { "md5": "607942b9b16b2cdc9324498a582c9a68", "sha256": "82081c998629b6a95f6c93ab1bea85ab96d6ca5bafada1faa9702fa144fd14d9" }, "downloads": -1, "filename": "dj_bgfiles-0.6.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "607942b9b16b2cdc9324498a582c9a68", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 32826, "upload_time": "2018-02-05T09:15:17", "url": "https://files.pythonhosted.org/packages/6a/87/385ddb2721bf49701cc936f778984f7c6556b7b4045491579cf8be9c8e42/dj_bgfiles-0.6.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "6e4c1afc4c69475b62e618c8b483d100", "sha256": "7feb47ac055149cc8dbead968a8f213444ed1ea68fb8ba4cdf5b10233a4ffe99" }, "downloads": -1, "filename": "dj-bgfiles-0.6.0.tar.gz", "has_sig": false, "md5_digest": "6e4c1afc4c69475b62e618c8b483d100", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42220, "upload_time": "2018-02-05T09:15:23", "url": "https://files.pythonhosted.org/packages/9d/22/63c14c460e22d34b9a7b6e3d1178c9c787b3b9f79aa59852e7c316742808/dj-bgfiles-0.6.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "607942b9b16b2cdc9324498a582c9a68", "sha256": "82081c998629b6a95f6c93ab1bea85ab96d6ca5bafada1faa9702fa144fd14d9" }, "downloads": -1, "filename": "dj_bgfiles-0.6.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "607942b9b16b2cdc9324498a582c9a68", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 32826, "upload_time": "2018-02-05T09:15:17", "url": "https://files.pythonhosted.org/packages/6a/87/385ddb2721bf49701cc936f778984f7c6556b7b4045491579cf8be9c8e42/dj_bgfiles-0.6.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "6e4c1afc4c69475b62e618c8b483d100", "sha256": "7feb47ac055149cc8dbead968a8f213444ed1ea68fb8ba4cdf5b10233a4ffe99" }, "downloads": -1, "filename": "dj-bgfiles-0.6.0.tar.gz", "has_sig": false, "md5_digest": "6e4c1afc4c69475b62e618c8b483d100", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 42220, "upload_time": "2018-02-05T09:15:23", "url": "https://files.pythonhosted.org/packages/9d/22/63c14c460e22d34b9a7b6e3d1178c9c787b3b9f79aa59852e7c316742808/dj-bgfiles-0.6.0.tar.gz" } ] }