Skip to content

Commit e5dc958

Browse files
feat(elasticsearch): added elasticsearch session interface
pallets-eco#105
1 parent c589d82 commit e5dc958

File tree

10 files changed

+269
-13
lines changed

10 files changed

+269
-13
lines changed

.coveragerc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[run]
2+
branch = True
3+
4+
[report]
5+
show_missing = True
6+
skip_covered = True
7+
omit =
8+
test_session.py

.github/workflows/test.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ jobs:
3535
uses: supercharge/redis-github-action@1.4.0
3636
with:
3737
redis-version: 6
38+
- name: Configure sysctl limits for elasticsearch
39+
run: |
40+
sudo swapoff -a
41+
sudo sysctl -w vm.swappiness=1
42+
sudo sysctl -w fs.file-max=262144
43+
sudo sysctl -w vm.max_map_count=262144
44+
- name: Start elasticsearch
45+
uses: getong/elasticsearch-action@v1.2
46+
with:
47+
elasticsearch version: 8.3.3
48+
host port: 9200
49+
container port: 9200
50+
host node port: 9300
51+
node port: 9300
52+
discovery type: single-node
3853
- name: install deps
3954
run: python -m pip install tox poetry tox-poetry
4055
- name: test

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
.python-version
22
node_modules/
3-
# Byte-compiled / optim
3+
# Byte-compiled / optimized / DLL files
44
.mypy_cache
5+
.DS_Store
56

6-
ized / DLL files
77
__pycache__/
88
*.py[cod]
99

.releaserc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
"plugins": [
44
"@semantic-release/commit-analyzer",
55
"@semantic-release/release-notes-generator",
6-
"@semantic-release/changelog",
6+
["@semantic-release/changelog",
7+
{
8+
"changelogFile": "CHANGELOG.md"
9+
}
10+
],
711
"@semantic-release/npm",
812
["@semantic-release/exec", {
913
"prepareCmd" : "poetry version ${nextRelease.version}"

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ Uses SQLAlchemy as a session backend. ([Flask-SQLAlchemy](https://pythonhosted.o
154154
- SESSION_SQLALCHEMY
155155
- SESSION_SQLALCHEMY_TABLE
156156

157+
### `ElasticsearchSessionInterface`
158+
159+
Uses elasticsearch as a session backend. ([elasticsearch](https://elasticsearch-py.readthedocs.io/en/v8.3.3/) required)
160+
161+
- SESSION_ELASTICSEARCH
162+
- SESSION_ELASTICSEARCH_HOST
163+
- SESSION_ELASTICSEARCH_INDEX
164+
157165
## Credits
158166

159167
This project is a fork of [flask-session](https://github.com/fengsp/flask-session), created by [Shipeng Feng](https://github.com/fengsp).

flask_session/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import os
1414

1515
from .sessions import (
16+
ElasticsearchSessionInterface,
1617
FileSystemSessionInterface,
1718
MemcachedSessionInterface,
1819
MongoDBSessionInterface,
@@ -89,6 +90,9 @@ def _get_interface(self, app):
8990
config.setdefault("SESSION_MONGODB_DB", "flask_session")
9091
config.setdefault("SESSION_MONGODB_COLLECT", "sessions")
9192
config.setdefault("SESSION_MONGODB_TZ_AWARE", False)
93+
config.setdefault("SESSION_ELASTICSEARCH", None)
94+
config.setdefault("SESSION_ELASTICSEARCH_HOST", "http://localhost:9200")
95+
config.setdefault("SESSION_ELASTICSEARCH_INDEX", "sessions")
9296
config.setdefault("SESSION_SQLALCHEMY", None)
9397
config.setdefault("SESSION_SQLALCHEMY_TABLE", "sessions")
9498
config.setdefault("SESSION_SQLALCHEMY_SEQUENCE", None)
@@ -137,6 +141,15 @@ def _get_interface(self, app):
137141
config["SESSION_PERMANENT"],
138142
config["SESSION_SQLALCHEMY_SEQUENCE"],
139143
)
144+
elif config["SESSION_TYPE"] == "elasticsearch":
145+
session_interface = ElasticsearchSessionInterface(
146+
config["SESSION_ELASTICSEARCH"],
147+
config["SESSION_ELASTICSEARCH_HOST"],
148+
config["SESSION_ELASTICSEARCH_INDEX"],
149+
config["SESSION_KEY_PREFIX"],
150+
config["SESSION_USE_SIGNER"],
151+
config["SESSION_PERMANENT"],
152+
)
140153
else:
141154
session_interface = NullSessionInterface()
142155

flask_session/sessions.py

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ class MongoDBSession(ServerSideSession):
6666
pass
6767

6868

69+
class ElasticsearchSession(ServerSideSession):
70+
pass
71+
72+
6973
class SqlAlchemySession(ServerSideSession):
7074
pass
7175

@@ -221,12 +225,6 @@ def __init__(self, app, client, key_prefix, use_signer=False, permanent=True):
221225

222226
def _get_preferred_memcache_client(self, app):
223227
server = "127.0.0.1:11211"
224-
# try:
225-
# import pylibmc
226-
# except ImportError:
227-
# pass
228-
# else:
229-
# return pylibmc.Client(servers)
230228

231229
try:
232230
import pymemcache
@@ -587,13 +585,13 @@ class Session(self.db.Model):
587585
__tablename__ = table
588586

589587
if sequence:
590-
id = self.db.Column(
588+
id = self.db.Column( # noqa: A003, VNE003, A001
591589
self.db.Integer, self.db.Sequence(sequence), primary_key=True
592590
)
593591
else:
594-
id = self.db.Column(
592+
id = self.db.Column( # noqa: A003, VNE003, A001
595593
self.db.Integer, primary_key=True
596-
) # noqa: A003, VNE003
594+
)
597595

598596
session_id = self.db.Column(self.db.String(255), unique=True)
599597
data = self.db.Column(self.db.LargeBinary)
@@ -698,3 +696,107 @@ def save_session(self, app, session, response):
698696
secure=secure,
699697
**conditional_cookie_kwargs,
700698
)
699+
700+
701+
class ElasticsearchSessionInterface(SessionInterface):
702+
"""A Session interface that uses Elasticsearch as backend.
703+
.. versionadded:: 0.X
704+
:param client: A ``elasticsearch.Elasticsearch`` instance.
705+
:param host: The elasticsearch host url you want to use.
706+
:param index: The elasticsearch index you want to use.
707+
:param key_prefix: A prefix that is added to all MongoDB store keys.
708+
:param use_signer: Whether to sign the session id cookie or not.
709+
:param permanent: Whether to use permanent session or not.
710+
"""
711+
712+
serializer = None
713+
session_class = ElasticsearchSession
714+
715+
def __init__(
716+
self, client, host, index, key_prefix, use_signer=False, permanent=True
717+
):
718+
if client is None:
719+
from elasticsearch import Elasticsearch
720+
721+
client = Elasticsearch(host)
722+
723+
self.client = client
724+
self.index = index
725+
try: # noqa: SIM105
726+
self.client.indices.create(index=self.index)
727+
except:
728+
pass
729+
self.key_prefix = key_prefix
730+
self.use_signer = use_signer
731+
self.permanent = permanent
732+
733+
def open_session(self, app, request):
734+
sid = request.cookies.get(app.config["SESSION_COOKIE_NAME"])
735+
if not sid:
736+
sid = self._generate_sid()
737+
return self.session_class(sid=sid, permanent=self.permanent)
738+
if self.use_signer:
739+
signer = self._get_signer(app)
740+
741+
if signer is None:
742+
return None
743+
744+
try:
745+
sid_as_bytes = signer.unsign(sid)
746+
sid = sid_as_bytes.decode()
747+
except BadSignature:
748+
sid = self._generate_sid()
749+
return self.session_class(sid=sid, permanent=self.permanent)
750+
751+
store_id = self.key_prefix + sid
752+
document = self.client.get(index=self.index, id=store_id, ignore=404)
753+
if document["found"]:
754+
expiration = document["_source"]["expiration"]
755+
756+
expiration = datetime.strptime(expiration, "%Y-%m-%dT%H:%M:%S.%f%z")
757+
if expiration <= datetime.utcnow().replace(tzinfo=pytz.UTC):
758+
# Delete expired session
759+
self.client.delete(index=self.index, id=store_id)
760+
document = None
761+
if document is not None:
762+
try:
763+
value = document["_source"]["val"]
764+
return self.session_class(value, sid=sid)
765+
except:
766+
return self.session_class(sid=sid, permanent=self.permanent)
767+
return self.session_class(sid=sid, permanent=self.permanent)
768+
769+
def save_session(self, app, session, response):
770+
domain = self.get_cookie_domain(app)
771+
path = self.get_cookie_path(app)
772+
store_id = self.key_prefix + session.sid
773+
if not session:
774+
if session.modified:
775+
self.client.delete(index=self.index, id=store_id)
776+
response.delete_cookie(
777+
app.config["SESSION_COOKIE_NAME"], domain=domain, path=path
778+
)
779+
return
780+
781+
httponly = self.get_cookie_httponly(app)
782+
secure = self.get_cookie_secure(app)
783+
expires = self.get_expiration_time(app, session)
784+
value = dict(session)
785+
self.client.index(
786+
index=self.index,
787+
id=store_id,
788+
body={"id": store_id, "val": value, "expiration": expires},
789+
)
790+
if self.use_signer:
791+
session_id = self._get_signer(app).sign(want_bytes(session.sid))
792+
else:
793+
session_id = session.sid
794+
response.set_cookie(
795+
app.config["SESSION_COOKIE_NAME"],
796+
session_id,
797+
expires=expires,
798+
httponly=httponly,
799+
domain=domain,
800+
path=path,
801+
secure=secure,
802+
)

poetry.lock

Lines changed: 80 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)