starsessions


Namestarsessions JSON
Version 2.1.3 PyPI version JSON
download
home_pagehttps://github.com/alex-oleshkevich/starsessions
SummaryAdvanced sessions for Starlette and FastAPI frameworks
upload_time2023-09-28 10:19:05
maintainer
docs_urlNone
authoralex.oleshkevich
requires_python>=3.8.0,<4.0.0
licenseMIT
keywords starlette fastapi asgi session
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            ## Starsessions

Advanced sessions for Starlette and FastAPI frameworks

![PyPI](https://img.shields.io/pypi/v/starsessions)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/alex-oleshkevich/starsessions/lint_and_test.yml?branch=master)
![GitHub](https://img.shields.io/github/license/alex-oleshkevich/starsessions)
![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/pypi/starsessions)
![PyPI - Downloads](https://img.shields.io/pypi/dm/starsessions)
![GitHub Release Date](https://img.shields.io/github/release-date/alex-oleshkevich/starsessions)

## Installation

Install `starsessions` using PIP or poetry:

```bash
pip install starsessions
# or
poetry add starsessions
```

Use `redis` extra for [Redis support](#redis).

## Quick start

See example application in [`examples/`](examples) directory of this repository.

## Usage

1. Add `starsessions.SessionMiddleware` to your application to enable session support,
2. Configure session store and pass it to the middleware,
3. Load session in your view/middleware by calling `load_session(connection)` utility.

```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.responses import JSONResponse
from starlette.routing import Route

from starsessions import CookieStore, load_session, SessionMiddleware


async def index_view(request):
    await load_session(request)

    session_data = request.session
    return JSONResponse(session_data)


session_store = CookieStore(secret_key='TOP SECRET')

app = Starlette(
    middleware=[
        Middleware(SessionMiddleware, store=session_store, lifetime=3600 * 24 * 14),
    ],
    routes=[
        Route('/', index_view),
    ]
)
```

### Cookie security

By default, the middleware uses strict defaults.
The cookie lifetime is limited to the browser session and sent via HTTPS protocol only.
You can change these defaults by changing `cookie_https_only` and `lifetime` arguments:

```python
from starlette.middleware import Middleware

from starsessions import CookieStore, SessionMiddleware

session_store = CookieStore(secret_key='TOP SECRET')

middleware = [
    Middleware(SessionMiddleware, store=session_store, cookie_https_only=False, lifetime=3600 * 24 * 14),
]
```

The example above will let session usage over insecure HTTP transport and the session lifetime will be set to 14 days.

### Loading session

The session data is not loaded by default. Call `load_session` to load data from the store.

```python
async def index_view(request):
    await load_session(request)
    request.session['key'] = 'value'
```

However, if you try to access uninitialized session, `SessionNotLoaded` exception will be raised.

```python
async def index_view(request):
    request.session['key'] = 'value'  # raises SessionNotLoaded
```

You can automatically load session by using `SessionAutoloadMiddleware` middleware.

### Session autoload

For performance reasons session is not autoloaded by default. Sometimes it is annoying to call `load_session` too often.
We provide `SessionAutoloadMiddleware` to reduce amount of boilerplate code by autoloading session for you.

There are two options: always autoload or autoload for specific paths only.
Here are examples:

```python
from starlette.middleware import Middleware

from starsessions import CookieStore, SessionAutoloadMiddleware, SessionMiddleware

session_store = CookieStore(secret_key='TOP SECRET')

# Always autoload

middleware = [
    Middleware(SessionMiddleware, store=session_store),
    Middleware(SessionAutoloadMiddleware),
]

# Autoload session for selected paths

middleware = [
    Middleware(SessionMiddleware, store=session_store),
    Middleware(SessionAutoloadMiddleware, paths=['/admin', '/app']),
]

# regex patterns also supported
import re

admin_rx = re.compile('/admin*')

middleware = [
    Middleware(SessionMiddleware, store=session_store),
    Middleware(SessionAutoloadMiddleware, paths=[admin_rx]),
]
```

### Rolling sessions

The default behavior of `SessionMiddleware` is to expire cookie after `lifetime` seconds after it was set.
For example, if you create a session with `lifetime=3600` then the session will be terminated exactly in 3600 seconds.
Sometimes this may not be what you need, so we provide alternate expiration strategy - rolling sessions.

When rolling sessions in use, the cookie expiration time will be extended by `lifetime` value on every response.
Let's see how it works on example. First, on the first response you create a new session with `lifetime=3600`,
then user does another request and session gets extended by another 3600 seconds and so on.
This approach is useful when you want to have short-timed sessions but don't want them to interrupt in the middle of
user's operation. With rolling strategy, session cookie will be expired only after some period of user's inactivity.

To enable rolling strategy set `rolling=True`.

```python
from starlette.middleware import Middleware
from starsessions import SessionMiddleware

middleware = [
    Middleware(SessionMiddleware, lifetime=300, rolling=True),
]
```

The snippet above demonstrates an example setup where session will be dropped after 300 seconds (5 minutes) of
inactivity, but will be automatically extended by another 5 minutes while the user is online.

### Cookie path

You can pass `cookie_path` argument to bind session cookie to specific URLs. For example, to activate session cookie
only for admin area, use `cookie_path="/admin"` middleware argument.

```python
from starlette.middleware import Middleware
from starsessions import SessionMiddleware

middleware = [
    Middleware(SessionMiddleware, cookie_path='/admin'),
]
```

All other URLs not matching value of `cookie_path` will not receive cookie thus session will be unavailable.

### Cookie domain

You can also specify which hosts can receive a cookie by passing `cookie_domain` argument to the middleware.

```python
from starlette.middleware import Middleware
from starsessions import SessionMiddleware

middleware = [
    Middleware(SessionMiddleware, cookie_domain='example.com'),
]
```

> Note, this makes session cookie available for subdomains too.
> For example, when you set `cookie_domain=example.com` then session cookie will be available on subdomains
> like `app.example.com`.

### Session-only cookies

If you want session cookie to automatically remove from tbe browser when tab closes then set `lifetime` to `0`.
> Note, this depends on browser implementation!

```python
from starlette.middleware import Middleware
from starsessions import SessionMiddleware

middleware = [
    Middleware(SessionMiddleware, lifetime=0),
]
```

## Built-in stores

### Memory

Class: `starsessions.InMemoryStore`

Simply stores data in memory. The data is cleared after server restart. Mostly for use with unit tests.

### CookieStore

Class: `starsessions.CookieStore`

Stores session data in a signed cookie on the client.

### Redis

Class: `starsessions.stores.redis.RedisStore`

Stores session data in a Redis server. The store accepts either connection URL or an instance of `Redis`.

> Requires [redis-py](https://github.com/redis/redis-py),
> use `pip install starsessions[redis]` or `poetry add starsessions[redis]`

```python
from redis.asyncio.utils import from_url

from starsessions.stores.redis import RedisStore

store = RedisStore('redis://localhost')
# or
redis = from_url('redis://localhost')

store = RedisStore(connection=redis)
```

#### Redis key prefix

By default, all keys in Redis prefixed with `starsessions.`. If you want to change this use `prefix` argument.

```python
from starsessions.stores.redis import RedisStore

store = RedisStore(url='redis://localhost', prefix='my_sessions')
```

Prefix can be a callable:

```python
from starsessions.stores.redis import RedisStore


def make_prefix(key: str) -> str:
    return 'my_sessions_' + key


store = RedisStore(url='redis://localhost', prefix=make_prefix)
```

#### Key expiration

The library automatically manages key expiration, usually you have nothing to do with it.
But for cases when `lifetime=0` we don't know when the session will over, and we have to heuristically calculate TTL
otherwise the data will remain in Redis forever. At this moment, we just set 30 days TTL. You can change it by
setting `gc_ttl` value on the store.

```python
from starsessions.stores.redis import RedisStore

store = RedisStore(url='redis://localhost', gc_ttl=3600)  # max 1 hour
```

## Custom store

Creating new stores is quite simple. All you need is to extend `starsessions.SessionStore`
class and implement abstract methods.

Here is an example of how we can create a memory-based session store. Note, it is important that `write` method
returns session ID as a string value.

```python
from typing import Dict

from starsessions import SessionStore


# instance of class which manages session persistence

class InMemoryStore(SessionStore):
    def __init__(self):
        self._storage = {}

    async def read(self, session_id: str, lifetime: int) -> Dict:
        """ Read session data from a data source using session_id. """
        return self._storage.get(session_id, {})

    async def write(self, session_id: str, data: Dict, lifetime: int, ttl: int) -> str:
        """ Write session data into data source and return session id. """
        self._storage[session_id] = data
        return session_id

    async def remove(self, session_id: str):
        """ Remove session data. """
        del self._storage[session_id]

    async def exists(self, session_id: str) -> bool:
        return session_id in self._storage
```

### lifetime and ttl

The `write` accepts two special arguments: `lifetime` and `ttl`.
The difference is that `lifetime` is a total session duration (set by the middleware)
and `ttl` is a remaining session time. After `ttl` seconds the data can be safely deleted from the storage.

> Your custom backend has to correctly handle setups when `lifetime = 0`.
In such cases you don't have exact expiration value, and you have to find a way how to extend session TTL at the storage
side, if any.

## Serializers

The library automatically serializes session data to string using JSON.
By default, we use `starsessions.JsonSerializer` but you can implement your own by extending `starsessions.Serializer`
class.

```python
import json
import typing

from starlette.middleware import Middleware

from starsessions import Serializer, SessionMiddleware


class MySerializer(Serializer):
    def serialize(self, data: typing.Any) -> bytes:
        return json.dumps(data).encode('utf-8')

    def deserialize(self, data: bytes) -> typing.Dict[str, typing.Any]:
        return json.loads(data)


middleware = [
    Middleware(SessionMiddleware, serializer=MySerializer()),
]
```

## Session termination

The middleware will remove session data and cookie if session has no data. Use `request.session.clear` to empty data.

## Regenerating session ID

Sometimes you need a new session ID to avoid session fixation attacks (for example, after successful signs in).
For that, use `starsessions.session.regenerate_session_id(connection)` utility.

```python
from starsessions.session import regenerate_session_id
from starlette.responses import Response


def login(request):
    regenerate_session_id(request)
    return Response('successfully signed in')
```

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/alex-oleshkevich/starsessions",
    "name": "starsessions",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8.0,<4.0.0",
    "maintainer_email": "",
    "keywords": "starlette,fastapi,asgi,session",
    "author": "alex.oleshkevich",
    "author_email": "alex.oleshkevich@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/5c/d2/e94111f00fb8ade82ee4ce6a2b2d2d7c40e283d9d83341957891641efd28/starsessions-2.1.3.tar.gz",
    "platform": null,
    "description": "## Starsessions\n\nAdvanced sessions for Starlette and FastAPI frameworks\n\n![PyPI](https://img.shields.io/pypi/v/starsessions)\n![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/alex-oleshkevich/starsessions/lint_and_test.yml?branch=master)\n![GitHub](https://img.shields.io/github/license/alex-oleshkevich/starsessions)\n![Libraries.io dependency status for latest release](https://img.shields.io/librariesio/release/pypi/starsessions)\n![PyPI - Downloads](https://img.shields.io/pypi/dm/starsessions)\n![GitHub Release Date](https://img.shields.io/github/release-date/alex-oleshkevich/starsessions)\n\n## Installation\n\nInstall `starsessions` using PIP or poetry:\n\n```bash\npip install starsessions\n# or\npoetry add starsessions\n```\n\nUse `redis` extra for [Redis support](#redis).\n\n## Quick start\n\nSee example application in [`examples/`](examples) directory of this repository.\n\n## Usage\n\n1. Add `starsessions.SessionMiddleware` to your application to enable session support,\n2. Configure session store and pass it to the middleware,\n3. Load session in your view/middleware by calling `load_session(connection)` utility.\n\n```python\nfrom starlette.applications import Starlette\nfrom starlette.middleware import Middleware\nfrom starlette.responses import JSONResponse\nfrom starlette.routing import Route\n\nfrom starsessions import CookieStore, load_session, SessionMiddleware\n\n\nasync def index_view(request):\n    await load_session(request)\n\n    session_data = request.session\n    return JSONResponse(session_data)\n\n\nsession_store = CookieStore(secret_key='TOP SECRET')\n\napp = Starlette(\n    middleware=[\n        Middleware(SessionMiddleware, store=session_store, lifetime=3600 * 24 * 14),\n    ],\n    routes=[\n        Route('/', index_view),\n    ]\n)\n```\n\n### Cookie security\n\nBy default, the middleware uses strict defaults.\nThe cookie lifetime is limited to the browser session and sent via HTTPS protocol only.\nYou can change these defaults by changing `cookie_https_only` and `lifetime` arguments:\n\n```python\nfrom starlette.middleware import Middleware\n\nfrom starsessions import CookieStore, SessionMiddleware\n\nsession_store = CookieStore(secret_key='TOP SECRET')\n\nmiddleware = [\n    Middleware(SessionMiddleware, store=session_store, cookie_https_only=False, lifetime=3600 * 24 * 14),\n]\n```\n\nThe example above will let session usage over insecure HTTP transport and the session lifetime will be set to 14 days.\n\n### Loading session\n\nThe session data is not loaded by default. Call `load_session` to load data from the store.\n\n```python\nasync def index_view(request):\n    await load_session(request)\n    request.session['key'] = 'value'\n```\n\nHowever, if you try to access uninitialized session, `SessionNotLoaded` exception will be raised.\n\n```python\nasync def index_view(request):\n    request.session['key'] = 'value'  # raises SessionNotLoaded\n```\n\nYou can automatically load session by using `SessionAutoloadMiddleware` middleware.\n\n### Session autoload\n\nFor performance reasons session is not autoloaded by default. Sometimes it is annoying to call `load_session` too often.\nWe provide `SessionAutoloadMiddleware` to reduce amount of boilerplate code by autoloading session for you.\n\nThere are two options: always autoload or autoload for specific paths only.\nHere are examples:\n\n```python\nfrom starlette.middleware import Middleware\n\nfrom starsessions import CookieStore, SessionAutoloadMiddleware, SessionMiddleware\n\nsession_store = CookieStore(secret_key='TOP SECRET')\n\n# Always autoload\n\nmiddleware = [\n    Middleware(SessionMiddleware, store=session_store),\n    Middleware(SessionAutoloadMiddleware),\n]\n\n# Autoload session for selected paths\n\nmiddleware = [\n    Middleware(SessionMiddleware, store=session_store),\n    Middleware(SessionAutoloadMiddleware, paths=['/admin', '/app']),\n]\n\n# regex patterns also supported\nimport re\n\nadmin_rx = re.compile('/admin*')\n\nmiddleware = [\n    Middleware(SessionMiddleware, store=session_store),\n    Middleware(SessionAutoloadMiddleware, paths=[admin_rx]),\n]\n```\n\n### Rolling sessions\n\nThe default behavior of `SessionMiddleware` is to expire cookie after `lifetime` seconds after it was set.\nFor example, if you create a session with `lifetime=3600` then the session will be terminated exactly in 3600 seconds.\nSometimes this may not be what you need, so we provide alternate expiration strategy - rolling sessions.\n\nWhen rolling sessions in use, the cookie expiration time will be extended by `lifetime` value on every response.\nLet's see how it works on example. First, on the first response you create a new session with `lifetime=3600`,\nthen user does another request and session gets extended by another 3600 seconds and so on.\nThis approach is useful when you want to have short-timed sessions but don't want them to interrupt in the middle of\nuser's operation. With rolling strategy, session cookie will be expired only after some period of user's inactivity.\n\nTo enable rolling strategy set `rolling=True`.\n\n```python\nfrom starlette.middleware import Middleware\nfrom starsessions import SessionMiddleware\n\nmiddleware = [\n    Middleware(SessionMiddleware, lifetime=300, rolling=True),\n]\n```\n\nThe snippet above demonstrates an example setup where session will be dropped after 300 seconds (5 minutes) of\ninactivity, but will be automatically extended by another 5 minutes while the user is online.\n\n### Cookie path\n\nYou can pass `cookie_path` argument to bind session cookie to specific URLs. For example, to activate session cookie\nonly for admin area, use `cookie_path=\"/admin\"` middleware argument.\n\n```python\nfrom starlette.middleware import Middleware\nfrom starsessions import SessionMiddleware\n\nmiddleware = [\n    Middleware(SessionMiddleware, cookie_path='/admin'),\n]\n```\n\nAll other URLs not matching value of `cookie_path` will not receive cookie thus session will be unavailable.\n\n### Cookie domain\n\nYou can also specify which hosts can receive a cookie by passing `cookie_domain` argument to the middleware.\n\n```python\nfrom starlette.middleware import Middleware\nfrom starsessions import SessionMiddleware\n\nmiddleware = [\n    Middleware(SessionMiddleware, cookie_domain='example.com'),\n]\n```\n\n> Note, this makes session cookie available for subdomains too.\n> For example, when you set `cookie_domain=example.com` then session cookie will be available on subdomains\n> like `app.example.com`.\n\n### Session-only cookies\n\nIf you want session cookie to automatically remove from tbe browser when tab closes then set `lifetime` to `0`.\n> Note, this depends on browser implementation!\n\n```python\nfrom starlette.middleware import Middleware\nfrom starsessions import SessionMiddleware\n\nmiddleware = [\n    Middleware(SessionMiddleware, lifetime=0),\n]\n```\n\n## Built-in stores\n\n### Memory\n\nClass: `starsessions.InMemoryStore`\n\nSimply stores data in memory. The data is cleared after server restart. Mostly for use with unit tests.\n\n### CookieStore\n\nClass: `starsessions.CookieStore`\n\nStores session data in a signed cookie on the client.\n\n### Redis\n\nClass: `starsessions.stores.redis.RedisStore`\n\nStores session data in a Redis server. The store accepts either connection URL or an instance of `Redis`.\n\n> Requires [redis-py](https://github.com/redis/redis-py),\n> use `pip install starsessions[redis]` or `poetry add starsessions[redis]`\n\n```python\nfrom redis.asyncio.utils import from_url\n\nfrom starsessions.stores.redis import RedisStore\n\nstore = RedisStore('redis://localhost')\n# or\nredis = from_url('redis://localhost')\n\nstore = RedisStore(connection=redis)\n```\n\n#### Redis key prefix\n\nBy default, all keys in Redis prefixed with `starsessions.`. If you want to change this use `prefix` argument.\n\n```python\nfrom starsessions.stores.redis import RedisStore\n\nstore = RedisStore(url='redis://localhost', prefix='my_sessions')\n```\n\nPrefix can be a callable:\n\n```python\nfrom starsessions.stores.redis import RedisStore\n\n\ndef make_prefix(key: str) -> str:\n    return 'my_sessions_' + key\n\n\nstore = RedisStore(url='redis://localhost', prefix=make_prefix)\n```\n\n#### Key expiration\n\nThe library automatically manages key expiration, usually you have nothing to do with it.\nBut for cases when `lifetime=0` we don't know when the session will over, and we have to heuristically calculate TTL\notherwise the data will remain in Redis forever. At this moment, we just set 30 days TTL. You can change it by\nsetting `gc_ttl` value on the store.\n\n```python\nfrom starsessions.stores.redis import RedisStore\n\nstore = RedisStore(url='redis://localhost', gc_ttl=3600)  # max 1 hour\n```\n\n## Custom store\n\nCreating new stores is quite simple. All you need is to extend `starsessions.SessionStore`\nclass and implement abstract methods.\n\nHere is an example of how we can create a memory-based session store. Note, it is important that `write` method\nreturns session ID as a string value.\n\n```python\nfrom typing import Dict\n\nfrom starsessions import SessionStore\n\n\n# instance of class which manages session persistence\n\nclass InMemoryStore(SessionStore):\n    def __init__(self):\n        self._storage = {}\n\n    async def read(self, session_id: str, lifetime: int) -> Dict:\n        \"\"\" Read session data from a data source using session_id. \"\"\"\n        return self._storage.get(session_id, {})\n\n    async def write(self, session_id: str, data: Dict, lifetime: int, ttl: int) -> str:\n        \"\"\" Write session data into data source and return session id. \"\"\"\n        self._storage[session_id] = data\n        return session_id\n\n    async def remove(self, session_id: str):\n        \"\"\" Remove session data. \"\"\"\n        del self._storage[session_id]\n\n    async def exists(self, session_id: str) -> bool:\n        return session_id in self._storage\n```\n\n### lifetime and ttl\n\nThe `write` accepts two special arguments: `lifetime` and `ttl`.\nThe difference is that `lifetime` is a total session duration (set by the middleware)\nand `ttl` is a remaining session time. After `ttl` seconds the data can be safely deleted from the storage.\n\n> Your custom backend has to correctly handle setups when `lifetime = 0`.\nIn such cases you don't have exact expiration value, and you have to find a way how to extend session TTL at the storage\nside, if any.\n\n## Serializers\n\nThe library automatically serializes session data to string using JSON.\nBy default, we use `starsessions.JsonSerializer` but you can implement your own by extending `starsessions.Serializer`\nclass.\n\n```python\nimport json\nimport typing\n\nfrom starlette.middleware import Middleware\n\nfrom starsessions import Serializer, SessionMiddleware\n\n\nclass MySerializer(Serializer):\n    def serialize(self, data: typing.Any) -> bytes:\n        return json.dumps(data).encode('utf-8')\n\n    def deserialize(self, data: bytes) -> typing.Dict[str, typing.Any]:\n        return json.loads(data)\n\n\nmiddleware = [\n    Middleware(SessionMiddleware, serializer=MySerializer()),\n]\n```\n\n## Session termination\n\nThe middleware will remove session data and cookie if session has no data. Use `request.session.clear` to empty data.\n\n## Regenerating session ID\n\nSometimes you need a new session ID to avoid session fixation attacks (for example, after successful signs in).\nFor that, use `starsessions.session.regenerate_session_id(connection)` utility.\n\n```python\nfrom starsessions.session import regenerate_session_id\nfrom starlette.responses import Response\n\n\ndef login(request):\n    regenerate_session_id(request)\n    return Response('successfully signed in')\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Advanced sessions for Starlette and FastAPI frameworks",
    "version": "2.1.3",
    "project_urls": {
        "Documentation": "https://github.com/alex-oleshkevich/starsessions",
        "Homepage": "https://github.com/alex-oleshkevich/starsessions",
        "Repository": "https://github.com/alex-oleshkevich/starsessions"
    },
    "split_keywords": [
        "starlette",
        "fastapi",
        "asgi",
        "session"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "aa26f0cac14f6cdcf70654550b2c6d6e7e5976a83053dcf25322ddadd2c2563d",
                "md5": "e0e91aa3ddc2d22510ea1a074ebc9269",
                "sha256": "5c2aa725ab7466e0b27c827a95c0a436179f0f12f8d826ef2c554e7f50a9a79f"
            },
            "downloads": -1,
            "filename": "starsessions-2.1.3-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "e0e91aa3ddc2d22510ea1a074ebc9269",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8.0,<4.0.0",
            "size": 14647,
            "upload_time": "2023-09-28T10:19:03",
            "upload_time_iso_8601": "2023-09-28T10:19:03.944224Z",
            "url": "https://files.pythonhosted.org/packages/aa/26/f0cac14f6cdcf70654550b2c6d6e7e5976a83053dcf25322ddadd2c2563d/starsessions-2.1.3-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "5cd2e94111f00fb8ade82ee4ce6a2b2d2d7c40e283d9d83341957891641efd28",
                "md5": "b1a52d79411e20777cc533881b7aeb6f",
                "sha256": "d20c5f277b64a86c16819f65ac50814ccdbd146776159e08c88b378b6612297d"
            },
            "downloads": -1,
            "filename": "starsessions-2.1.3.tar.gz",
            "has_sig": false,
            "md5_digest": "b1a52d79411e20777cc533881b7aeb6f",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8.0,<4.0.0",
            "size": 14856,
            "upload_time": "2023-09-28T10:19:05",
            "upload_time_iso_8601": "2023-09-28T10:19:05.461919Z",
            "url": "https://files.pythonhosted.org/packages/5c/d2/e94111f00fb8ade82ee4ce6a2b2d2d7c40e283d9d83341957891641efd28/starsessions-2.1.3.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-09-28 10:19:05",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "alex-oleshkevich",
    "github_project": "starsessions",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "starsessions"
}
        
Elapsed time: 0.12605s