Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Binding DB sessions based on SQLAlchemy 1, changing how to declare Base Model classes, and other code modernization #11

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased](https://github.com/python-social-auth/social-storage-sqlalchemy/commits/master)

### Changed
- Modified model and access code to work with SQLAlchemy version 2 (Issue #9)
- Updated packaging information files per PEP 517, PEP 518 (Issue #10)
- Restricted Python minimum working version to 3.7 or higher to align with SQLAlchemy 2 (Issue #9)

## [1.1.0](https://github.com/python-social-auth/social-storage-sqlalchemy/releases/tag/1.1.0) - 2017-05-06

### Changed
Expand Down
8 changes: 2 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
build:
@ python setup.py sdist
@ python setup.py bdist_wheel --python-tag py2
@ BUILD_VERSION=3 python setup.py bdist_wheel --python-tag py3
@ python -m build

publish:
@ python setup.py sdist upload
@ python setup.py bdist_wheel --python-tag py2 upload
@ BUILD_VERSION=3 python setup.py bdist_wheel --python-tag py3 upload
@ twine upload dist/*

clean:
@ find . -name '*.py[co]' -delete
Expand Down
53 changes: 53 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = 'social-auth-storage-sqlalchemy'
dynamic = ["version"]
dependencies = [
"six",
"sqlalchemy",
"social-auth-core>=1.0.0",
]
authors = [
{name = "Matias Aguirre", email = "matiasaguirre@gmail.com"},
{name = "Lee Ji-ho", email = "search5@gmail.com"},
]
description = 'Python Social Authentication, SQLAlchemy storage.'
license = {text = 'BSD'}
keywords = ["sqlalchemy", "social auth"]
readme = "README.md"
classifiers=[
'Development Status :: 4 - Beta',
'Topic :: Internet',
'License :: OSI Approved :: BSD License',
'Intended Audience :: Developers',
'Environment :: Web Environment',
'Programming Language :: Python',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12'
]
requires-python = ">= 3.7"

[project.urls]
Repository = 'https://github.com/python-social-auth/social-storage-sqlalchemy'
Documentation = 'http://python-social-auth.readthedocs.org'
Issues = 'https://github.com/python-social-auth/social-storage-sqlalchemy/issues'
Changelog = 'https://github.com/python-social-auth/social-storage-sqlalchemy/blob/master/CHANGELOG.md

[options]
zip_safe = false

[tool.setuptools]
include-package-data = true

[tool.setuptools.packages]
find = {}

[tool.setuptools.dynamic]
version = {attr = "social_sqlalchemy.__version__"}
3 changes: 0 additions & 3 deletions requirements.txt

This file was deleted.

36 changes: 0 additions & 36 deletions setup.py

This file was deleted.

111 changes: 64 additions & 47 deletions social_sqlalchemy/storage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""SQLAlchemy models for Social Auth"""
import base64
from typing import Optional

import six
import json

Expand All @@ -8,11 +10,12 @@
except ImportError:
transaction = None

from sqlalchemy import Column, Integer, String
from sqlalchemy import select, delete, String, func, Integer
from sqlalchemy.exc import IntegrityError
from sqlalchemy.types import PickleType, Text
from sqlalchemy.schema import UniqueConstraint
from sqlalchemy.ext.declarative import declared_attr

from sqlalchemy.orm import declared_attr, Mapped, mapped_column
from sqlalchemy.ext.mutable import MutableDict

from social_core.storage import UserMixin, AssociationMixin, NonceMixin, \
Expand Down Expand Up @@ -51,7 +54,7 @@ def _session(cls):

@classmethod
def _query(cls):
return cls._session().query(cls)
return select(cls)

@classmethod
def _new_instance(cls, model, *args, **kwargs):
Expand Down Expand Up @@ -86,15 +89,15 @@ class SQLAlchemyUserMixin(SQLAlchemyMixin, UserMixin):
"""Social Auth association model"""
__tablename__ = 'social_auth_usersocialauth'
__table_args__ = (UniqueConstraint('provider', 'uid'),)
id = Column(Integer, primary_key=True)
provider = Column(String(32))
uid = None
id: Mapped[int] = mapped_column(primary_key=True)
provider: Mapped[str] = mapped_column(String(32))
uid: Mapped[str] = mapped_column(String(255))
user_id = None
user = None

@declared_attr
def extra_data(cls):
return Column(MutableDict.as_mutable(JSONType))
def extra_data(cls) -> Mapped[Optional[dict[str, str]]]:
return mapped_column(MutableDict.as_mutable(JSONType))

@classmethod
def changed(cls, user):
Expand All @@ -107,16 +110,22 @@ def set_extra_data(self, extra_data=None):
@classmethod
def allowed_to_disconnect(cls, user, backend_name, association_id=None):
if association_id is not None:
qs = cls._query().filter(cls.id != association_id)
qs = cls._query().where(cls.id != association_id)
else:
qs = cls._query().filter(cls.provider != backend_name)
qs = qs.filter(cls.user == user)
qs = cls._query().where(cls.provider != backend_name)
qs = qs.where(cls.user == user)

if hasattr(user, 'has_usable_password'): # TODO
valid_password = user.has_usable_password()
else:
valid_password = True
return valid_password or qs.count() > 0

qs_count = cls._session().scalar(
select(func.count()).
select_from(qs.subquery())
)

return valid_password or qs_count > 0

@classmethod
def disconnect(cls, entry):
Expand All @@ -125,15 +134,22 @@ def disconnect(cls, entry):

@classmethod
def user_query(cls):
return cls._session().query(cls.user_model())
return select(cls.user_model())

@classmethod
def user_exists(cls, *args, **kwargs):
"""
Return True/False if a User instance exists with the given arguments.
Arguments are directly passed to filter() manager method.
"""
return cls.user_query().filter_by(*args, **kwargs).count() > 0
stmt = cls.user_query().filter_by(*args, **kwargs)

user_count = cls._session().scalar(
select(func.count()).
select_from(stmt.subquery())
)

return user_count > 0

@classmethod
def get_username(cls, user):
Expand All @@ -145,19 +161,19 @@ def create_user(cls, *args, **kwargs):

@classmethod
def get_user(cls, pk):
return cls.user_query().get(pk)
return cls._session().get(cls.user_model(), pk)

@classmethod
def get_users_by_email(cls, email):
return cls.user_query().filter_by(email=email)
return cls._session().scalar(cls.user_query().filter_by(email=email))

@classmethod
def get_social_auth(cls, provider, uid):
if not isinstance(uid, six.string_types):
uid = str(uid)
try:
return cls._query().filter_by(provider=provider,
uid=uid)[0]
return cls._session().scalar(
cls._query().filter_by(provider=provider, uid=uid))
except IndexError:
return None

Expand All @@ -168,7 +184,7 @@ def get_social_auth_for_user(cls, user, provider=None, id=None):
qs = qs.filter_by(provider=provider)
if id:
qs = qs.filter_by(id=id)
return qs
return cls._session().scalars(qs)

@classmethod
def create_social_auth(cls, user, uid, provider):
Expand All @@ -180,38 +196,39 @@ def create_social_auth(cls, user, uid, provider):
class SQLAlchemyNonceMixin(SQLAlchemyMixin, NonceMixin):
__tablename__ = 'social_auth_nonce'
__table_args__ = (UniqueConstraint('server_url', 'timestamp', 'salt'),)
id = Column(Integer, primary_key=True)
server_url = Column(String(255))
timestamp = Column(Integer)
salt = Column(String(40))

id: Mapped[int] = mapped_column(primary_key=True)
server_url: Mapped[str] = mapped_column(String(255))
timestamp: Mapped[int] = mapped_column(Integer)
salt: Mapped[str] = mapped_column(String(40))

@classmethod
def use(cls, server_url, timestamp, salt):
kwargs = {'server_url': server_url, 'timestamp': timestamp,
'salt': salt}
try:
return cls._query().filter_by(**kwargs)[0]
return cls._session().scalar(cls._query().filter_by(**kwargs))
except IndexError:
return cls._new_instance(cls, **kwargs)


class SQLAlchemyAssociationMixin(SQLAlchemyMixin, AssociationMixin):
__tablename__ = 'social_auth_association'
__table_args__ = (UniqueConstraint('server_url', 'handle'),)
id = Column(Integer, primary_key=True)
server_url = Column(String(255))
handle = Column(String(255))
secret = Column(String(255)) # base64 encoded
issued = Column(Integer)
lifetime = Column(Integer)
assoc_type = Column(String(64))
id: Mapped[int] = mapped_column(primary_key=True)
server_url: Mapped[str] = mapped_column(String(255))
handle: Mapped[str] = mapped_column(String(255))
secret: Mapped[str] = mapped_column(String(255)) # base64 encoded
issued: Mapped[int] = mapped_column()
lifetime: Mapped[int] = mapped_column()
assoc_type: Mapped[str] = mapped_column(String(64))

@classmethod
def store(cls, server_url, association):
# Don't use get_or_create because issued cannot be null
try:
assoc = cls._query().filter_by(server_url=server_url,
handle=association.handle)[0]
assoc = cls._session().scalar(cls._query().filter_by(server_url=server_url,
handle=association.handle))
except IndexError:
assoc = cls(server_url=server_url,
handle=association.handle)
Expand All @@ -223,38 +240,38 @@ def store(cls, server_url, association):

@classmethod
def get(cls, *args, **kwargs):
return cls._query().filter_by(*args, **kwargs)
return cls._session().scalar(cls._query().filter_by(*args, **kwargs))

@classmethod
def remove(cls, ids_to_delete):
cls._query().filter(cls.id.in_(ids_to_delete)).delete(
synchronize_session='fetch'
)
cls._session().execute(delete(
cls._query().where(cls.id.in_(ids_to_delete))
).execution_options(synchronize_session="fetch"))


class SQLAlchemyCodeMixin(SQLAlchemyMixin, CodeMixin):
__tablename__ = 'social_auth_code'
__table_args__ = (UniqueConstraint('code', 'email'),)
id = Column(Integer, primary_key=True)
email = Column(String(200))
code = Column(String(32), index=True)
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(200))
code: Mapped[str] = mapped_column(String(32), index=True)

@classmethod
def get_code(cls, code):
return cls._query().filter(cls.code == code).first()
return cls._session().scalar(cls._query().where(cls.code == code))


class SQLAlchemyPartialMixin(SQLAlchemyMixin, PartialMixin):
__tablename__ = 'social_auth_partial'
id = Column(Integer, primary_key=True)
token = Column(String(32), index=True)
data = Column(MutableDict.as_mutable(JSONType))
next_step = Column(Integer)
backend = Column(String(32))
id: Mapped[int] = mapped_column(primary_key=True)
token: Mapped[str] = mapped_column(String(32), index=True)
data: Mapped[dict[str, str]] = mapped_column(MutableDict.as_mutable(JSONType))
next_step: Mapped[int] = mapped_column()
backend: Mapped[str] = mapped_column(String(32))

@classmethod
def load(cls, token):
return cls._query().filter(cls.token == token).first()
return cls._session().scalar(cls._query().where(cls.token == token))

@classmethod
def destroy(cls, token):
Expand Down