Skip to content

Commit a703ce2

Browse files
authored
Merge pull request #685 from wwwjfy/mysql-support
mysql support
2 parents 810c989 + 5db5874 commit a703ce2

34 files changed

+3863
-69
lines changed

.codacy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
exclude_paths:
22
- 'tests/**'
3+
- 'mysql_tests/**'
34
- 'docs/**'

.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[run]
22
source =
33
./src
4+
omit =
5+
./src/gino/aiocontextvars.py
46
[report]
57
exclude_lines =
68
pragma: no cover

.github/workflows/test.yml

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,127 @@ jobs:
6666
DB_HOST: localhost
6767
DB_USER: gino
6868
run: |
69-
$HOME/.poetry/bin/poetry run pytest --cov=src --cov-fail-under=95 --cov-report xml
69+
$HOME/.poetry/bin/poetry run pytest tests/
70+
test-mysql:
71+
runs-on: ubuntu-latest
72+
strategy:
73+
matrix:
74+
python-version: [ '3.5', '3.6', '3.7', '3.8' ]
75+
deps-version: [ 'lowest', 'highest' ]
76+
services:
77+
mysql:
78+
image: mysql:5
79+
env:
80+
MYSQL_ALLOW_EMPTY_PASSWORD: 1
81+
ports:
82+
- 3306:3306
83+
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
84+
steps:
85+
- name: Checkout source code
86+
uses: actions/checkout@v1
87+
- name: Set up Python
88+
uses: actions/setup-python@v1
89+
with:
90+
python-version: ${{ matrix.python-version }}
91+
- name: virtualenv cache
92+
uses: actions/cache@preview
93+
with:
94+
path: ~/.cache/pypoetry/virtualenvs
95+
key: ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.deps-version }}-venv-${{ hashFiles(format('{0}{1}', github.workspace, '/poetry.lock')) }}
96+
restore-keys: |
97+
${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.deps-version }}-venv-
98+
- name: Poetry cache
99+
uses: actions/cache@preview
100+
with:
101+
path: ~/.poetry
102+
key: ${{ runner.os }}-${{ matrix.python-version }}-dotpoetry
103+
restore-keys: |
104+
${{ runner.os }}-${{ matrix.python-version }}-dotpoetry-
105+
- name: Install Python dependencies
106+
run: |
107+
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
108+
$HOME/.poetry/bin/poetry install --no-interaction
109+
- name: Use lowest dependencies versions
110+
if: matrix.deps-version == 'lowest'
111+
run: |
112+
$HOME/.poetry/bin/poetry run pip install asyncpg==0.18 SQLAlchemy==1.3
113+
- name: List installed packages
114+
run: |
115+
$HOME/.poetry/bin/poetry run pip list
116+
- name: Test with pytest
117+
env:
118+
MYSQL_DB_HOST: 127.0.0.1
119+
MYSQL_DB_USER: root
120+
run: |
121+
$HOME/.poetry/bin/poetry run pytest mysql_tests/
122+
summary:
123+
runs-on: ubuntu-latest
124+
services:
125+
postgres:
126+
image: fantix/postgres-ssl:12.1
127+
env:
128+
POSTGRES_USER: gino
129+
ports:
130+
- 5432:5432
131+
# needed because the postgres container does not provide a healthcheck
132+
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
133+
mysql:
134+
image: mysql:5
135+
env:
136+
MYSQL_ALLOW_EMPTY_PASSWORD: 1
137+
ports:
138+
- 3306:3306
139+
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
140+
steps:
141+
- name: Checkout source code
142+
uses: actions/checkout@v1
143+
- name: Set up Python
144+
uses: actions/setup-python@v1
145+
with:
146+
python-version: 3.8
147+
- name: virtualenv cache
148+
uses: actions/cache@preview
149+
with:
150+
path: ~/.cache/pypoetry/virtualenvs
151+
key: ${{ runner.os }}-3.8-highest-venv-${{ hashFiles(format('{0}{1}', github.workspace, '/poetry.lock')) }}
152+
restore-keys: |
153+
${{ runner.os }}-3.8-highest-venv-
154+
- name: Poetry cache
155+
uses: actions/cache@preview
156+
with:
157+
path: ~/.poetry
158+
key: ${{ runner.os }}-3.8-dotpoetry
159+
restore-keys: |
160+
${{ runner.os }}-3.8-dotpoetry-
161+
- name: Install Python dependencies
162+
run: |
163+
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
164+
$HOME/.poetry/bin/poetry install --no-interaction
165+
- name: List installed packages
166+
run: |
167+
$HOME/.poetry/bin/poetry run pip list
168+
- name: Test with pytest
169+
env:
170+
DB_HOST: localhost
171+
DB_USER: gino
172+
MYSQL_DB_HOST: 127.0.0.1
173+
MYSQL_DB_USER: root
174+
run: |
175+
$HOME/.poetry/bin/poetry run pytest --cov=src --cov-fail-under=95 --cov-report xml tests/ mysql_tests/
70176
- name: Check code format with black
71-
if: matrix.python-version >= '3.6'
72177
run: |
73178
$HOME/.poetry/bin/poetry run black --check src
74179
- name: Submit coverage report
75-
if: matrix.python-version == '3.8' && matrix.postgres-version == '12.1' && matrix.deps-version == 'highest' && github.ref == 'refs/heads/master'
180+
if: github.ref == 'refs/heads/master'
76181
env:
77182
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_TOKEN }}
78183
run: |
79184
pip install codacy-coverage
80185
python-codacy-coverage -r coverage.xml
186+
81187
release:
82188
runs-on: ubuntu-latest
83-
needs: test
189+
needs: summary
84190
strategy:
85191
matrix:
86192
python-version: [ '3.8' ]

mysql_tests/__init__.py

Whitespace-only changes.

mysql_tests/conftest.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import ssl
2+
3+
import aiomysql
4+
import pytest
5+
import sqlalchemy
6+
from async_generator import yield_, async_generator
7+
8+
import gino
9+
from .models import db, DB_ARGS, MYSQL_URL, random_name
10+
11+
ECHO = False
12+
13+
14+
@pytest.fixture(scope="module")
15+
def sa_engine():
16+
rv = sqlalchemy.create_engine(MYSQL_URL, echo=ECHO)
17+
db.create_all(rv)
18+
yield rv
19+
db.drop_all(rv)
20+
rv.dispose()
21+
22+
23+
@pytest.fixture
24+
@async_generator
25+
async def engine(sa_engine):
26+
e = await gino.create_engine(MYSQL_URL, echo=ECHO, minsize=10)
27+
await yield_(e)
28+
await e.close()
29+
sa_engine.execute("DELETE FROM gino_user_settings")
30+
sa_engine.execute("DELETE FROM gino_users")
31+
32+
33+
# noinspection PyUnusedLocal,PyShadowingNames
34+
@pytest.fixture
35+
@async_generator
36+
async def bind(sa_engine):
37+
async with db.with_bind(MYSQL_URL, echo=ECHO, minsize=10) as e:
38+
await yield_(e)
39+
sa_engine.execute("DELETE FROM gino_user_settings")
40+
sa_engine.execute("DELETE FROM gino_users")
41+
42+
43+
# noinspection PyUnusedLocal,PyShadowingNames
44+
@pytest.fixture
45+
@async_generator
46+
async def aiomysql_pool(sa_engine):
47+
async with aiomysql.create_pool(**DB_ARGS) as rv:
48+
await yield_(rv)
49+
async with rv.acquire() as conn:
50+
await conn.query("DELETE FROM gino_user_settings")
51+
await conn.query("DELETE FROM gino_users")
52+
53+
54+
@pytest.fixture
55+
def ssl_ctx():
56+
ctx = ssl.create_default_context()
57+
ctx.check_hostname = False
58+
ctx.verify_mode = ssl.CERT_NONE
59+
return ctx

mysql_tests/models.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import os
2+
import enum
3+
import random
4+
import string
5+
from datetime import datetime
6+
7+
import aiomysql
8+
import asyncpg
9+
import pytest
10+
11+
from gino import Gino
12+
from gino.dialects.aiomysql import JSON
13+
14+
DB_ARGS = dict(
15+
host=os.getenv("MYSQL_DB_HOST", "localhost"),
16+
port=os.getenv("MYSQL_DB_PORT", 3306),
17+
user=os.getenv("MYSQL_DB_USER", "root"),
18+
password=os.getenv("MYSQL_DB_PASS", ""),
19+
db=os.getenv("MYSQL_DB_NAME", "mysql"),
20+
)
21+
MYSQL_URL = "mysql://{user}:{password}@{host}:{port}/{db}".format(**DB_ARGS)
22+
db = Gino()
23+
24+
25+
@pytest.fixture
26+
def random_name(length=8) -> str:
27+
return _random_name(length)
28+
29+
30+
def _random_name(length=8):
31+
return "".join(random.choice(string.ascii_letters) for _ in range(length))
32+
33+
34+
class UserType(enum.Enum):
35+
USER = "USER"
36+
37+
38+
class User(db.Model):
39+
__tablename__ = "gino_users"
40+
41+
id = db.Column(db.BigInteger(), primary_key=True)
42+
nickname = db.Column("name", db.Unicode(255), default=_random_name)
43+
profile = db.Column("props", JSON(), nullable=False, default="{}")
44+
type = db.Column(db.Enum(UserType), nullable=False, default=UserType.USER)
45+
realname = db.StringProperty()
46+
age = db.IntegerProperty(default=18)
47+
balance = db.IntegerProperty(default=0)
48+
birthday = db.DateTimeProperty(default=lambda i: datetime.utcfromtimestamp(0))
49+
team_id = db.Column(db.ForeignKey("gino_teams.id"))
50+
51+
@balance.after_get
52+
def balance(self, val):
53+
if val is None:
54+
return 0.0
55+
return float(val)
56+
57+
def __repr__(self):
58+
return "{}<{}>".format(self.nickname, self.id)
59+
60+
61+
class Friendship(db.Model):
62+
__tablename__ = "gino_friendship"
63+
64+
my_id = db.Column(db.BigInteger(), primary_key=True)
65+
friend_id = db.Column(db.BigInteger(), primary_key=True)
66+
67+
def __repr__(self):
68+
return "Friends<{}, {}>".format(self.my_id, self.friend_id)
69+
70+
71+
class Relation(db.Model):
72+
__tablename__ = "gino_relation"
73+
74+
name = db.Column(db.VARCHAR(255), primary_key=True)
75+
76+
77+
class Team(db.Model):
78+
__tablename__ = "gino_teams"
79+
80+
id = db.Column(db.BigInteger(), primary_key=True)
81+
name = db.Column(db.Unicode(255), default=_random_name)
82+
parent_id = db.Column(db.ForeignKey("gino_teams.id", ondelete='CASCADE'))
83+
company_id = db.Column(db.ForeignKey("gino_companies.id"))
84+
85+
def __init__(self, **kw):
86+
super().__init__(**kw)
87+
self._members = set()
88+
89+
@property
90+
def members(self):
91+
return self._members
92+
93+
@members.setter
94+
def add_member(self, user):
95+
self._members.add(user)
96+
97+
98+
class TeamWithDefaultCompany(Team):
99+
company = Team(name="DEFAULT")
100+
101+
102+
class TeamWithoutMembersSetter(Team):
103+
def add_member(self, user):
104+
self._members.add(user)
105+
106+
107+
class Company(db.Model):
108+
__tablename__ = "gino_companies"
109+
110+
id = db.Column(db.BigInteger(), primary_key=True)
111+
name = db.Column(db.Unicode(255), default=_random_name)
112+
logo = db.Column(db.LargeBinary())
113+
114+
def __init__(self, **kw):
115+
super().__init__(**kw)
116+
self._teams = set()
117+
118+
@property
119+
def teams(self):
120+
return self._teams
121+
122+
@teams.setter
123+
def add_team(self, team):
124+
self._teams.add(team)
125+
126+
127+
class CompanyWithoutTeamsSetter(Company):
128+
def add_team(self, team):
129+
self._teams.add(team)
130+
131+
132+
class UserSetting(db.Model):
133+
__tablename__ = "gino_user_settings"
134+
135+
# No constraints defined on columns
136+
id = db.Column(db.BigInteger())
137+
user_id = db.Column(db.BigInteger())
138+
setting = db.Column(db.VARCHAR(255))
139+
value = db.Column(db.Text())
140+
col1 = db.Column(db.Integer, default=1)
141+
col2 = db.Column(db.Integer, default=2)
142+
143+
# Define indexes and constraints inline
144+
id_pkey = db.PrimaryKeyConstraint("id")
145+
user_id_fk = db.ForeignKeyConstraint(["user_id"], ["gino_users.id"])
146+
user_id_setting_unique = db.UniqueConstraint("user_id", "setting")
147+
col1_check = db.CheckConstraint("col1 >= 1 AND col1 <= 5")
148+
col2_idx = db.Index("col2_idx", "col2")
149+
150+
151+
def qsize(engine):
152+
if isinstance(engine.raw_pool, aiomysql.pool.Pool):
153+
return engine.raw_pool.freesize
154+
if isinstance(engine.raw_pool, asyncpg.pool.Pool):
155+
# noinspection PyProtectedMember
156+
return engine.raw_pool._queue.qsize()
157+
raise Exception('Unknown pool')

0 commit comments

Comments
 (0)