{ "info": { "author": "", "author_email": "", "bugtrack_url": null, "classifiers": [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "===========\nSocketShark\n===========\n.. image:: https://circleci.com/gh/closeio/socketshark/tree/master.svg?style=svg\n :target: https://circleci.com/gh/closeio/socketshark/tree/master\n\n*SocketShark* is a WebSocket message router based on Python/Redis/asyncio.\n\n(Interested in working on projects like this? `Close.io`_ is looking for `great engineers`_ to join our team)\n\n.. _Close.io: http://close.io\n.. _great engineers: http://jobs.close.io\n\n.. contents::\n\nSummary\n=======\n\nSocketShark makes it easy to build WebSocket-based services without requiring\nthose services to be aware of WebSockets. Instead, services implement HTTP\nendpoints for receiving messages from WebSocket clients, and publish messages\nto WebSocket clients via Redis, while SocketShark takes care of long-running\nWebSocket connections and passing messages between clients and services.\n\nFeatures\n========\n\n- Pub-sub messages\n\n SocketShark allows building applications relying on the publish-subscribe\n pattern without having to be aware of long-running WebSocket connections.\n Subscriptions and published messages from the WebSocket client are forwarded\n to the application via HTTP. Messages can be pushed from the application to\n the WebSocket client synchronously by pushing them to Redis.\n\n- Flexible WebSocket backend\n\n SocketShark comes with Websockets for Python 3 (websockets_) backend but can\n easily be adapted to other frameworks compatible with asyncio.\n\n- Multiple services\n\n Through its configuration file, SocketShark can work with any number of\n services.\n\n- Out-of-order message filtering\n\n If needed, an internal order can be supplied with messages from services, and\n SocketShark will automatically filter out out-of-order messages.\n\n- Message throttling\n\n If needed, service messages can be throttled by SocketShark.\n\n- Authentication\n\n SocketShark comes with ticket authentication built-in. To authenticate\n WebSocket connections, the client requests a temporary token from the app\n server and submits it when logging in. The token is then exchanged for a\n session/user ID that can be used to authenticate the user by the service\n backed by SocketShark.\n\n- Authorization\n\n Pub-sub subscriptions can be authorized using a custom HTTP endpoint.\n SocketShark can periodically reauthorize subscriptions to ensure subscribers\n are unsubscribed if they're no longer authorized.\n\n- Custom fields\n\n SocketShark supports custom application-specific fields for authentication\n and authorization purposes.\n\n- Metrics\n\n SocketShark keeps track and reports metrics such as connection counts and\n successfully or unsuccessfully executed commands, with built-in Prometheus\n and logging backends.\n\n.. _websockets: https://websockets.readthedocs.io/\n\nQuick start\n===========\n\nSee ``example_config.py`` for an example configuration file.\n\nTo start, install SocketShark (``python setup.py install``), create your own\nconfiguration file, and run SocketShark as follows:\n\n.. code:: bash\n\n PYTHONPATH=. socketshark -c my_config\n\nClient Protocol\n===============\n\nSocketShark uses WebSockets as the transport protocol for clients. This section\ndescribes the structure of the protocol between web clients and SocketShark.\n\nBoth clients and the server exchange JSON-messages. Each message is a JSON dict\ncontaining an ``event`` field which specifies the type of event. SocketShark\nsupports the following events:\n\n- ``auth``: Authentication\n- ``subscribe``: Subscribe to a topic\n- ``message``: Send a message to a topic\n- ``unsubscribe``: Unsubscribe from a topic\n\nResponses usually contain a ``status`` field which can be ``ok`` or ``error``.\nIn case of an error, an ``error`` field is supplied containing the error\ndescription as a string.\n\nAuthentication\n--------------\n\nWebSockets clients can authenticate using the ``auth`` event using ticket\nauthentication. For more information about ticket authentication see the\n`Ticket-based authentication for session-based apps`_ section.\n\nThe ``auth`` event takes an optional ``method`` (``ticket`` is the only\ncurrently supported authentication method, and the default), and a ``ticket``\nargument, containing the login ticket.\n\nExample client request:\n\n.. code:: json\n\n {\"event\": \"auth\", \"method\": \"ticket\", \"ticket\": \"SECRET_AUTH_TICKET\"}\n\nExample server responses (successful and unsuccessful):\n\n.. code:: json\n\n {\"event\": \"auth\", \"status\": \"ok\"}\n\n.. code:: json\n\n {\"event\": \"auth\", \"status\": \"error\", \"error\": \"Authentication failed.\"}\n\nSubscriptions\n-------------\n\nWebSocket clients can subscribe to any number of topics. Messages can be passed\nfrom the client to the server, and pushed from the server to the client at any\ntime while subscribed to a topic. For example, a client may subscribe to an\nobject ID, and the server may send a message whenever the object is updated.\nThe server may include extra data when subscribing or unsubscribing. For\nexample, the server might send the current state of the object when\nsubscribing.\n\nTopics are unique, and a client can be subscribed to each topic at most once.\nExtra fields can be associated with a subscription which are passed along with\nall subscription commands. For example, a client could be required to indicate\nthe organization ID for a particular object subscription so that the service\ncan authorize and process the message properly.\n\nSubscribe\n~~~~~~~~~\n\nThe ``subscribe`` event subscribes to a topic given in the ``subscription``\nargument, which is composed of the service name and the topic, separated by\nperiod. Extra fields can be defined by the service and directly specified in\nthe subscription message.\n\nExample client request:\n\n.. code:: json\n\n {\"event\": \"subscribe\", \"subscription\": \"books.book_1\"}\n\nExample server responses (successful and unsuccessful):\n\n.. code:: json\n\n {\"event\": \"subscribe\", \"subscription\": \"books.book_1\", \"status\": \"ok\"}\n\n.. code:: json\n\n {\n \"event\": \"subscribe\",\n \"subscription\": \"books.book_1\",\n \"status\": \"error\",\n \"error\": \"Book does not exist.\"\n }\n\nExample server response with extra data:\n\n.. code:: json\n\n {\n \"event\": \"subscribe\",\n \"subscription\": \"books.book_1\",\n \"status\": \"ok\",\n \"data\": {\n \"title\": \"Everyone poops\"\n }\n }\n\nExample client request with extra fields:\n\n.. code:: json\n\n {\"event\": \"subscribe\", \"subscription\": \"books.book_1\", \"author_id\": \"author_1\"}\n\nExample successful server responses with extra fields:\n\n.. code:: json\n\n {\n \"event\": \"subscribe\",\n \"subscription\": \"books.book_1\",\n \"author_id\": \"author_1\",\n \"status\": \"ok\"\n }\n\nNote that the subscription name is unique for the subscription. When subscribed\nto ``books.book_1`` we can't subscribe to another subcription with the same\nname even if the ``author_id`` is different. However, the server could use the\n``author_id`` to ensure the book matches the given author ID.\n\nMessage\n~~~~~~~\n\nOnce subscribed, the ``message`` event can be used to pass messages. Message\ndata is contained in the ``data`` field, and should be dicts. The structure of\nthe data is up to the application protocol, and the service decides whether\nmessages are confirmed (successfully or unsuccessfully).\n\nExample message (either client-to-server or server-to-client):\n\n.. code:: json\n\n {\n \"event\": \"message\",\n \"subscription\": \"books.book_1\",\n \"data\": {\n \"action\": \"update\",\n \"title\": \"New book title\"\n }\n }\n\nExample (optional) server-side message confirmation of a successful message\nwith extra data:\n\n.. code:: json\n\n {\n \"event\": \"message\",\n \"subscription\": \"books.book_1\",\n \"status\": \"ok\",\n \"data\": {\"status\": \"Book was updated.\"}\n }\n\n\nExample (optional) server-side message confirmation of a failed message:\n\n.. code:: json\n\n {\n \"event\": \"message\",\n \"subscription\": \"books.book_1\",\n \"status\": \"error\",\n \"error\": \"Book could not be updated.\"\n }\n\nIf extra fields are passed with the subscription, they are included in all\n``message`` events.\n\nNote that a service may send messages limited to particular authentication\nfields (e.g. limited to a specific user ID), so multiple sessions subscribed\nto the same topic may not necessarily receive the same messages.\n\nUnsubscribe\n~~~~~~~~~~~\n\nClients can unsubscribe from a topic using the ``unsubscribe`` event.\n\nExample client request:\n\n.. code:: json\n\n {\"event\": \"unsubscribe\", \"subscription\": \"books.book_1\"}\n\nExample server responses (successful and unsuccessful):\n\n.. code:: json\n\n {\"event\": \"unsubscribe\", \"subscription\": \"books.book_1\", \"status\": \"ok\"}\n\n.. code:: json\n\n {\n \"event\": \"unsubscribe\",\n \"subscription\": \"books.book_1\",\n \"status\": \"error\",\n \"error\": \"Subscription does not exist.\"\n }\n\nService Protocol\n================\n\nSocketShark uses HTTP to send events to services, and Redis PUBSUB to receive\nmessages from services that are published to subscribed clients. This section\ndescribes the structure of the protocol between services and SocketShark.\n\nHTTP callbacks\n--------------\n\nAn optional HTTP endpoint can be configured to authenticate a WebSocket\nsession. The authentication endpoint can return authentication-related fields\nthat can be configured (e.g. a user ID and/or session ID).\n\nThe following optional HTTP endpoints can be configured for each SocketShark\nservice:\n\n- ``authorizer``: URL to call to authorize a new subscription.\n- ``before_subscribe``: URL to call when a client attempts to subscribe.\n- ``on_subscribe``: URL to call after a client subscribed to a topic.\n- ``on_message``: URL to call when a client sends a message to a topic.\n- ``before_unsubscribe``: URL to call when a client attempts to unsubscribe.\n- ``on_unsubscribe``: URL to call after a client unsubscribed from a topic.\n- ``on_authorization_change``: URL to call after if any authorizer fields\n change during periodic authorization.\n\nEach HTTP endpoint is accessed via a POST request containing a JSON body.\n\nService-specific endpoints receive any client-supplied extra fields that are\nconfigured for the particular service, as well as authentication-related fields\nreturned by the authentication endpoint.\n\nHTTP endpoints should return a JSON dict containing a ``status`` field with the\nvalue ``ok`` or ``error``. In case of an error, an error text may be specified\nin the ``error`` field.\n\nAuthentication\n~~~~~~~~~~~~~~\n\nThe authentication URL receives JSON dict with the client's ticket supplied in\nthe ``ticket`` field. Only a successful response authenticates the user.\n\nExample request body:\n\n.. code:: json\n\n {\"ticket\": \"SECRET_AUTH_TICKET\"}\n\nExample server responses (successful with auth fields and unsuccessful):\n\n.. code:: json\n\n {\"status\": \"ok\", \"user_id\": \"user_1\", \"session_id\": \"session_1\"}\n\n.. code:: json\n\n {\"status\": \"error\", \"error\": \"Authentication failed.\"}\n\nAuthorization\n~~~~~~~~~~~~~\n\nIf an ``authorizer`` URL is supplied for a service, it is invoked each time a\nuser attempts to subscribe to a topic. Only a successful response authorizes\nthe subscription, triggering the ``before_subscribe`` callback (if specified).\n\nIf a service has no authorizer, all topics are authorized.\n\nExample request body (for an authenticated session with auth fields as well as\nextra client fields):\n\n.. code:: json\n\n {\n \"subscription\": \"books.book_1\",\n \"user_id\": \"user_1\",\n \"session_id\": \"session_1\",\n \"author_id\": \"author_1\"\n }\n\nExample server responses (successful and unsuccessful):\n\n.. code:: json\n\n {\"status\": \"ok\"}\n\n.. code:: json\n\n {\"status\": \"error\", \"error\": \"Author ID does not match book ID.\"}\n\nDuring an active subscription, SocketShark will periodically query the\nauthorizer endpoint if ``authorization_renewal_period`` is set to the number\nof seconds. The user will be unsubscribed by SocketShark if the authorization\nis no longer valid and an ``unsubscribe`` message will be sent to the client,\ne.g.:\n\n.. code:: json\n\n {\n \"event\": \"unsubscribe\",\n \"subscription\": \"books.book_1\",\n \"error\": \"Unauthorized.\"\n }\n\nBefore subscribe\n~~~~~~~~~~~~~~~~\n\nAfter a subscription is authorized, the ``before_subscribe`` callback is\ninvoked with the same arguments as the authorizer. Only a successful response\nconfirms the subscription, triggering the ``on_subscribe`` callback (if\nspecified).\n\nExtra data can be returned in this callback using the ``data`` field which is\nforwarded to the client. If returned, the ``data`` field should be a dict.\n\nOn subscribe\n~~~~~~~~~~~~\nAfter a subscription is confirmed, the ``on_subscribe`` callback is invoked\nwith the same arguments as the authorizer. An unsuccessful response doesn't\naffect the client's subscription.\n\nOn message\n~~~~~~~~~~\nWhen a client sends a message to the service, the ``on_message`` callback is\ninvoked with the same arguments as the authorizer, plus the message data in the\n``data`` field.\n\nA successful response with a ``data`` field, or an unsuccessful response\nsends a confirmation to the client.\n\nExample request body (for an authenticated session with auth fields as well as\nextra client fields supplied during the subscription):\n\n.. code:: json\n\n {\n \"subscription\": \"books.book_1\",\n \"user_id\": \"user_1\",\n \"session_id\": \"session_1\",\n \"author_id\": \"author_1\",\n \"data\": {\n \"action\": \"update\",\n \"title\": \"New book title\"\n }\n }\n\nExample server response (successful, triggers no response):\n\n.. code:: json\n\n {\"status\": \"ok\"}\n\nExample server response (successful, triggers a response):\n\n.. code:: json\n\n {\"status\": \"ok\", \"data\": {\"status\": \"Book was updated.\"}\n\nExample server response (unsuccessful, triggers a response):\n\n.. code:: json\n\n {\"status\": \"error\", \"error\": \"Book could not be updated.\"}\n\nBefore unsubscribe\n~~~~~~~~~~~~~~~~~~\nWhen a client issues an unsubscribe event, the ``before_unsubscribe`` callback\nis invoked with the same arguments as the authorizer. Only a successful\nresponse confirms the unsubscription, triggering the ``on_unsubscribe``\ncallback (if specified).\n\nExtra data can be returned in this callback using the ``data`` field which is\nforwarded to the client. If returned, the ``data`` field should be a dict.\n\nOn unsubscribe\n~~~~~~~~~~~~~~\nAfter an unsubscription is confirmed, the ``on_unsubscribe`` callback is\ninvoked with the same arguments as the authorizer. An unsuccessful response\ndoesn't affect the client's unsubscription.\n\nPublishing messages to clients\n------------------------------\nTo publish a message, a service needs to publish a Redis message to the\nappropriate subscription. The message must be JSON-formatted, and contain\nthe ``subscription`` field, a free-form ``data`` dict and any optional filters\n(if the service has configured filter fields). The channel name corresponds to\nthe subscription (``service.topic``), but a Redis channel prefix may be\noptionally configured.\n\nWhen a filter field is specified, the message is only published to sessions\nthat match the filter. For example, a message could only be sent to sessions\nmatching a specific user ID.\n\nExample Redis PUBLISH command:\n\n.. code:: json\n\n PUBLISH books.book_1 {\n \"subscription\": \"books.book_1\",\n \"data\": {\n \"action\": \"update\",\n \"title\": \"New title\"\n }\n }\n\nOut-of-order message filtering\n------------------------------\n\nSince messages published by services may not necessarily arrive in the desired\norder, SocketShark supports message filtering. For example, you might be\npublishing updates for a versioned object to Redis but they may arrive\nout-of-order due to network latency. Messages can be tagged with an order, and\nSocketShark will filter out older messages if a newer message arrives first. A\nfloat order can be supplied both in the `before_subscribe` callback's return\nvalue and in any published message using the `order` option in the `options`\ndict. Incoming messages with an order that is lower or equal to the last\nreceived highest order will be filtered out. Multiple independent orders can be\nspecified using the optional `order_key` option.\n\nIn the following example, the \"initiating\" and \"completed\" messages, as well as\nthe \"h\" and \"hello\" messages will be delivered to subscribers:\n\n.. code:: json\n\n PUBLISH calls.call_1 {\n \"subscription\": \"calls.call_1\",\n \"options\": {\n \"order\": 1,\n \"order_key\": \"call_1.status\",\n },\n \"data\": {\n \"status\": \"initiating\",\n }\n }\n\n PUBLISH calls.call_1 {\n \"subscription\": \"calls.call_1\",\n \"options\": {\n \"order\": 3,\n \"order_key\": \"call_1.status\",\n },\n \"data\": {\n \"status\": \"completed\",\n }\n }\n\n PUBLISH calls.call_1 {\n \"subscription\": \"calls.call_1\",\n \"options\": {\n \"order\": 2,\n \"order_key\": \"call_1.status\",\n },\n \"data\": {\n \"status\": \"ringing\",\n }\n }\n\n PUBLISH calls.call_1 {\n \"subscription\": \"calls.call_1\",\n \"options\": {\n \"order\": 1,\n \"order_key\": \"call_1.note\",\n },\n \"data\": {\n \"note\": \"h\",\n }\n }\n\n PUBLISH calls.call_1 {\n \"subscription\": \"calls.call_1\",\n \"options\": {\n \"order\": 3,\n \"order_key\": \"call_1.note\",\n },\n \"data\": {\n \"note\": \"hello\",\n }\n }\n\n PUBLISH calls.call_1 {\n \"subscription\": \"calls.call_1\",\n \"options\": {\n \"order\": 2,\n \"order_key\": \"call_1.note\",\n },\n \"data\": {\n \"note\": \"hell\",\n }\n }\n\nMessage throttling\n------------------\n\nMessages published by services can be throttled by specifying the time in\nseconds using the `throttle` option in the `options` dict in the published\nmessage.\n\nFor a constant stream of messages that are coming in shorter than the throttle\nperiod, the client will receive the first message immediately, then a message\nevery throttle period until the stream ends, and then the final message will be\nsent after another throttle period elapses.\n\nMultiple independent throttles can be specified using the optional\n`throttle_key` option. Throttling is performed per subscription per session.\n\nIn the example below, if the three messages are published at the same time, the\nfirst one will be delivered to subscribers immediately, the second one will be\nignored, and the third message will be delivered to subscribers after 100ms\npass.\n\n.. code:: json\n\n PUBLISH calls.stats {\n \"subscription\": \"calls.stats\",\n \"options\": {\n \"throttle\" 0.1,\n },\n \"data\": {\n \"n_calls\": 1,\n }\n }\n\n PUBLISH calls.stats {\n \"subscription\": \"calls.stats\",\n \"options\": {\n \"throttle\" 0.1,\n },\n \"data\": {\n \"n_calls\": 2,\n }\n }\n\n PUBLISH calls.stats {\n \"subscription\": \"calls.stats\",\n \"options\": {\n \"throttle\" 0.1,\n },\n \"data\": {\n \"n_calls\": 3,\n }\n }\n\n\nUse patterns\n============\n\nThis section illustrates how to implement common use patterns when building a\nservice with SocketShark.\n\nTicket-based authentication for session-based apps\n--------------------------------------------------\n\nMost web applications use an HTTP-only cookie that stores a session ID for\nauthentication. Since WebSocket connections are initiated via JavaScript, there\nis no access to the session ID via the cookie. To facilitate authentication of\nWebSocket connections, authentication with single-use tickets should be used:\n\n- Implement a public \"ticket\" endpoint in your application. The endpoint should\n validate the user's session and return a random-generated short-lived ticket\n associated to the user's session ID. For example, a UUID4 ticket may be\n computed and stored in Redis with a 30 second expiration using the SETEX\n command, where the key name corresponds to the ticket (the UUID4), and the\n key value is the user's session ID.\n\n- Implement an internal ticket validation in your application. The endpoint\n should be configured as the auth endpoint in SocketShark. It should retrieve\n and return the user's session ID, and at the same time invalidate the ticket.\n Any other user information (e.g. user ID) may also be returned. A Redis\n pipeline should be used to retrieve and delete the ticket.\n\n- When the JavaScript code connects to SocketShark, it should first request a\n ticket via the public ticket endpoint, then connect to SocketShark and issue\n the authentication event with the obtained ticket.\n\nAuthorization with microservices\n--------------------------------\n\nSuppose a user can access products from a set of authorized organization IDs.\nThe auth service stores a list of users and corresponding organization IDs that\nusers have access to. The product service stores a list of products with\ncorresponding organization IDs but is not aware whether a user is authorized to\naccess a specific organization (and therefore product). Subscriptions are per\nproduct and should only be authorized if the user can access the product's\norganization. To solve this problem without requiring the services to directly\ntalk to each other, extra fields can be used in SocketShark:\n\n- Add the user ID in SocketShark's authentication configuration under\n ``auth_fields``, and the organization ID as under the product service's\n ``extra_fields``.\n\n- Return the user ID in the auth service's authorization endpoint. SocketShark\n will supply it in all subsequent requests to service endpoints.\n\n- When the client subscribes to the product service (subscription example:\n ``product.PROD_ID``), it must also supply the product's organization ID as an\n extra field.\n\n- Set up an ``authorizer`` URL for the product service that points to the auth\n service. The auth service should authorize a subscription if the given user\n has access to the given organization. Since the authorizer doesn't have\n access to the product database, it doesn't validate the product ID.\n\n- Set up a ``before_subscribe`` URL for the product service that points to the\n product service. The product service should allow a subscription if the\n subscription's product ID matches the given organization ID. Since the\n organization ID is already validated by the authorizer, no further validation\n is necessary.\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://github.com/closeio/socketshark", "keywords": "", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "socketshark", "package_url": "https://pypi.org/project/socketshark/", "platform": "any", "project_url": "https://pypi.org/project/socketshark/", "project_urls": { "Homepage": "http://github.com/closeio/socketshark" }, "release_url": "https://pypi.org/project/socketshark/0.2.1/", "requires_dist": null, "requires_python": "", "summary": "WebSocket message router", "version": "0.2.1" }, "last_serial": 5104331, "releases": { "0.1": [ { "comment_text": "", "digests": { "md5": "1427208d2d96210eebe37c8ab75c66c2", "sha256": "f05301456233b94a008a107c8e2a5d638cebd9129a449396687e376b0e57fd17" }, "downloads": -1, "filename": "socketshark-0.1.tar.gz", "has_sig": false, "md5_digest": "1427208d2d96210eebe37c8ab75c66c2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 15126, "upload_time": "2017-06-09T22:09:52", "url": "https://files.pythonhosted.org/packages/62/a5/49b57aa92abfdfeb69e4da02293e9bd832b41723627859723ab20e331dbd/socketshark-0.1.tar.gz" } ], "0.2": [ { "comment_text": "", "digests": { "md5": "d6023e1a505c9a1622bf315ebc4f9cb3", "sha256": "5ece346b11740bd2416846808f9d7758cb57d82eb81cee2436e0099c16bbf8fd" }, "downloads": -1, "filename": "socketshark-0.2.tar.gz", "has_sig": false, "md5_digest": "d6023e1a505c9a1622bf315ebc4f9cb3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19742, "upload_time": "2018-08-15T14:55:10", "url": "https://files.pythonhosted.org/packages/33/90/3972942dcaaccbc30f23d230e5e90d980ed40d263d808b4a9da825f1cd40/socketshark-0.2.tar.gz" } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "4a191a5f191753af844ae28e41fb2dd9", "sha256": "4c6cdece4f87bb421001f1b76a304a611c5bb268a6e6c9bc0fa7bf399a9f01d4" }, "downloads": -1, "filename": "socketshark-0.2.1.tar.gz", "has_sig": false, "md5_digest": "4a191a5f191753af844ae28e41fb2dd9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27349, "upload_time": "2019-04-05T15:42:29", "url": "https://files.pythonhosted.org/packages/16/fe/4e35f63bf7d512384a256937b59e5740d79b7380d376d7a5d6d73cd2648e/socketshark-0.2.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "4a191a5f191753af844ae28e41fb2dd9", "sha256": "4c6cdece4f87bb421001f1b76a304a611c5bb268a6e6c9bc0fa7bf399a9f01d4" }, "downloads": -1, "filename": "socketshark-0.2.1.tar.gz", "has_sig": false, "md5_digest": "4a191a5f191753af844ae28e41fb2dd9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 27349, "upload_time": "2019-04-05T15:42:29", "url": "https://files.pythonhosted.org/packages/16/fe/4e35f63bf7d512384a256937b59e5740d79b7380d376d7a5d6d73cd2648e/socketshark-0.2.1.tar.gz" } ] }