Skip to content

Commit 2f5ef51

Browse files
authored
Merge branch 'master' into query-tag
2 parents c3fdce8 + 645fce0 commit 2f5ef51

16 files changed

+2069
-7
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[flake8]
22
ignore = B008,B023,B306,E203,E402,E731,D100,D101,D102,D103,D104,D105,W503,W504,E252,F999,F541
3-
exclude = .git,__pycache__,build,dist,.eggs
3+
exclude = .git,__pycache__,build,dist,.eggs,generated

.github/workflows/tests.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,15 @@ jobs:
7878

7979
- name: Install Python Deps
8080
if: steps.release.outputs.version == 0
81+
env:
82+
PYTHON_VERSION: ${{ matrix.python-version }}
8183
run: |
8284
python -m pip install --upgrade setuptools pip wheel
83-
python -m pip install -e .[test]
85+
if [ "${PYTHON_VERSION}" = "3.10" -o "${PYTHON_VERSION}" = "3.11" -o "${PYTHON_VERSION}" = "3.12" ]; then
86+
python -m pip install -e .[test,sqltest]
87+
else
88+
python -m pip install -e .[test]
89+
fi
8490
8591
- name: Test
8692
if: steps.release.outputs.version == 0

gel/_testbase.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import atexit
2222
import contextlib
2323
import functools
24+
import importlib.util
2425
import inspect
2526
import json
2627
import logging
@@ -35,6 +36,8 @@
3536
import gel
3637
from gel import asyncio_client
3738
from gel import blocking_client
39+
from gel.orm.introspection import get_schema_json
40+
from gel.orm.sqla import ModelGenerator
3841

3942

4043
log = logging.getLogger(__name__)
@@ -444,6 +447,7 @@ class DatabaseTestCase(ClusterTestCase, ConnectedTestCaseMixin):
444447
SETUP = None
445448
TEARDOWN = None
446449
SCHEMA = None
450+
DEFAULT_MODULE = 'test'
447451

448452
SETUP_METHOD = None
449453
TEARDOWN_METHOD = None
@@ -521,15 +525,18 @@ def get_database_name(cls):
521525
@classmethod
522526
def get_setup_script(cls):
523527
script = ''
528+
schema = []
524529

525530
# Look at all SCHEMA entries and potentially create multiple
526-
# modules, but always create the 'test' module.
527-
schema = ['\nmodule test {}']
531+
# modules, but always create the test module, if not `default`.
532+
if cls.DEFAULT_MODULE != 'default':
533+
schema.append(f'\nmodule {cls.DEFAULT_MODULE} {{}}')
528534
for name, val in cls.__dict__.items():
529535
m = re.match(r'^SCHEMA(?:_(\w+))?', name)
530536
if m:
531-
module_name = (m.group(1) or 'test').lower().replace(
532-
'__', '.')
537+
module_name = (
538+
m.group(1) or cls.DEFAULT_MODULE
539+
).lower().replace('_', '::')
533540

534541
with open(val, 'r') as sf:
535542
module = sf.read()
@@ -623,6 +630,54 @@ def adapt_call(cls, result):
623630
return result
624631

625632

633+
class SQLATestCase(SyncQueryTestCase):
634+
SQLAPACKAGE = None
635+
DEFAULT_MODULE = 'default'
636+
637+
@classmethod
638+
def setUpClass(cls):
639+
# SQLAlchemy relies on psycopg2 to connect to Postgres and thus we
640+
# need it to run tests. Unfortunately not all test environemnts might
641+
# have psycopg2 installed, as long as we run this in the test
642+
# environments that have this, it is fine since we're not expecting
643+
# different functionality based on flavours of psycopg2.
644+
if importlib.util.find_spec("psycopg2") is None:
645+
raise unittest.SkipTest("need psycopg2 for ORM tests")
646+
647+
super().setUpClass()
648+
649+
class_set_up = os.environ.get('EDGEDB_TEST_CASES_SET_UP')
650+
if not class_set_up:
651+
# Now that the DB is setup, generate the SQLAlchemy models from it
652+
spec = get_schema_json(cls.client)
653+
# We'll need a temp directory to setup the generated Python
654+
# package
655+
cls.tmpsqladir = tempfile.TemporaryDirectory()
656+
gen = ModelGenerator(
657+
outdir=os.path.join(cls.tmpsqladir.name, cls.SQLAPACKAGE),
658+
basemodule=cls.SQLAPACKAGE,
659+
)
660+
gen.render_models(spec)
661+
sys.path.append(cls.tmpsqladir.name)
662+
663+
@classmethod
664+
def tearDownClass(cls):
665+
super().tearDownClass()
666+
# cleanup the temp modules
667+
sys.path.remove(cls.tmpsqladir.name)
668+
cls.tmpsqladir.cleanup()
669+
670+
@classmethod
671+
def get_dsn_for_sqla(cls):
672+
cargs = cls.get_connect_args(database=cls.get_database_name())
673+
dsn = (
674+
f'postgresql://{cargs["user"]}:{cargs["password"]}'
675+
f'@{cargs["host"]}:{cargs["port"]}/{cargs["database"]}'
676+
)
677+
678+
return dsn
679+
680+
626681
_lock_cnt = 0
627682

628683

gel/orm/__init__.py

Whitespace-only changes.

gel/orm/cli.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#
2+
# This source file is part of the EdgeDB open source project.
3+
#
4+
# Copyright 2024-present MagicStack Inc. and the EdgeDB authors.
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
20+
import argparse
21+
22+
import gel
23+
24+
from gel.codegen.generator import _get_conn_args
25+
from .introspection import get_schema_json
26+
from .sqla import ModelGenerator
27+
28+
29+
class ArgumentParser(argparse.ArgumentParser):
30+
def error(self, message):
31+
self.exit(
32+
2,
33+
f"error: {message:s}\n",
34+
)
35+
36+
37+
parser = ArgumentParser(
38+
description="Generate Python ORM code for accessing a Gel database."
39+
)
40+
parser.add_argument(
41+
"orm",
42+
choices=['sqlalchemy', 'django'],
43+
help="Pick which ORM to generate models for.",
44+
)
45+
parser.add_argument("--dsn")
46+
parser.add_argument("--credentials-file", metavar="PATH")
47+
parser.add_argument("-I", "--instance", metavar="NAME")
48+
parser.add_argument("-H", "--host")
49+
parser.add_argument("-P", "--port")
50+
parser.add_argument("-d", "--database", metavar="NAME")
51+
parser.add_argument("-u", "--user")
52+
parser.add_argument("--password")
53+
parser.add_argument("--password-from-stdin", action="store_true")
54+
parser.add_argument("--tls-ca-file", metavar="PATH")
55+
parser.add_argument(
56+
"--tls-security",
57+
choices=["default", "strict", "no_host_verification", "insecure"],
58+
)
59+
parser.add_argument(
60+
"--out",
61+
help="The output directory for the generated files.",
62+
required=True,
63+
)
64+
parser.add_argument(
65+
"--mod",
66+
help="The fullname of the Python module corresponding to the output "
67+
"directory.",
68+
required=True,
69+
)
70+
71+
72+
def main():
73+
args = parser.parse_args()
74+
# setup client
75+
client = gel.create_client(**_get_conn_args(args))
76+
spec = get_schema_json(client)
77+
78+
match args.orm:
79+
case 'sqlalchemy':
80+
gen = ModelGenerator(
81+
outdir=args.out,
82+
basemodule=args.mod,
83+
)
84+
gen.render_models(spec)
85+
case 'django':
86+
print('Not available yet. Coming soon!')

0 commit comments

Comments
 (0)