Skip to content

Commit 11e1e89

Browse files
committed
fix relation target for foreign tree api building
1 parent 22c6518 commit 11e1e89

17 files changed

Lines changed: 2986 additions & 70 deletions

src/fastapi_quickcrud/crud_router.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ async def async_runner(f):
152152
foreign_table_mapping = {db_model.__tablename__: db_model}
153153
if foreign_include:
154154
for i in foreign_include:
155-
foreign_table_mapping[i.__tablename__] = i
155+
model , _= convert_table_to_model(i)
156+
foreign_table_mapping[model.__tablename__] = i
156157
crud_service = query_service(model=db_model, async_mode=async_mode, foreign_table_mapping=foreign_table_mapping)
157158
# else:
158159
# crud_service = SQLAlchemyPostgreQueryService(model=db_model, async_mode=async_mode)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from sqlalchemy.ext.declarative import declarative_base
2+
3+
from sqlalchemy.sql.schema import Table
4+
5+
6+
def convert_table_to_model(db_model):
7+
NO_PRIMARY_KEY = False
8+
if not isinstance(db_model, Table):
9+
return db_model, NO_PRIMARY_KEY
10+
db_name = str(db_model.fullname)
11+
table_dict = {'__table__': db_model,
12+
'__tablename__': db_name}
13+
14+
if not db_model.primary_key:
15+
table_dict['__mapper_args__'] = {
16+
"primary_key": [i for i in db_model._columns]
17+
}
18+
NO_PRIMARY_KEY = True
19+
20+
for i in db_model.c:
21+
col, = i.expression.base_columns
22+
table_dict[str(i.key)] = col
23+
24+
return type(f'{db_name}DeclarativeBaseClass', (declarative_base(),), table_dict), NO_PRIMARY_KEY

src/fastapi_quickcrud/misc/schema_builder.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from sqlalchemy.orm import declarative_base
2828
from strenum import StrEnum
2929

30+
from .covert_model import convert_table_to_model
3031
from .exceptions import (SchemaException,
3132
ColumnTypeNotSupportedException)
3233
from .type import (MatchingPatternInStringBase,
@@ -182,6 +183,7 @@ def __foreign_mapper_builder(self):
182183
if self.exclude_primary_key:
183184
return foreign_mapper
184185
for db_model in self.foreign_include:
186+
db_model, NO_PRIMARY_KEY = convert_table_to_model(db_model)
185187
tmp = {}
186188
table_name = self.__get_table_name(db_model)
187189
tmp["model"] = db_model
@@ -489,12 +491,15 @@ def _extra_relation_level(self, model=None, processed_table=None) -> Dict[str, T
489491
mapper = inspect(model)
490492
relation_level = []
491493
for r in mapper.relationships:
492-
relation_table = r.key
493-
if relation_table and relation_table not in processed_table and relation_table in self.foreign_mapper:
494+
495+
target_table = r.target
496+
target_model, _ = convert_table_to_model(target_table)
497+
target_table_name = target_model.__tablename__
498+
if target_table_name and target_table_name not in processed_table and target_table_name in self.foreign_mapper:
494499
processed_table.append(str(mapper.local_table))
495-
if self.foreign_mapper[relation_table]["db_name"] not in relation_level:
496-
relation_level.append(self.foreign_mapper[relation_table]["db_name"])
497-
relation_level += self._extra_relation_level(self.foreign_mapper[relation_table]["db_model"],
500+
if self.foreign_mapper[target_table_name]["db_name"] not in relation_level:
501+
relation_level.append(self.foreign_mapper[target_table_name]["db_name"])
502+
relation_level += self._extra_relation_level(self.foreign_mapper[target_table_name]["db_model"],
498503
processed_table=processed_table
499504
)
500505
return relation_level
@@ -505,7 +510,6 @@ def _extra_foreign_table_from_declarative_base(self, model) -> Dict[str, Table]:
505510
reference_mapper = {}
506511
for r in mapper.relationships:
507512
local, = r.local_columns
508-
relation_table = r.key
509513
local = mapper.get_property_by_column(local).expression
510514
local_table = str(local).split('.')[0]
511515
local_column = str(local).split('.')[1]

src/fastapi_quickcrud/misc/utils.py

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
BinaryExpression
1010

1111
from sqlalchemy.sql.schema import Table
12+
13+
from .covert_model import convert_table_to_model
1214
from .crud_model import RequestResponseModel, CRUDModel
1315
from .exceptions import QueryOperatorNotFound, PrimaryMissing, UnknownColumn
1416
from .schema_builder import ApiParameterSchemaBuilder
@@ -337,27 +339,6 @@ def group_by_foreign_key(item):
337339
return response_list
338340

339341

340-
def convert_table_to_model(db_model):
341-
NO_PRIMARY_KEY = False
342-
if not isinstance(db_model, Table):
343-
return db_model, NO_PRIMARY_KEY
344-
db_name = str(db_model.fullname)
345-
table_dict = {'__table__': db_model,
346-
'__tablename__': db_name}
347-
348-
if not db_model.primary_key:
349-
table_dict['__mapper_args__'] = {
350-
"primary_key": [i for i in db_model._columns]
351-
}
352-
NO_PRIMARY_KEY = True
353-
354-
for i in db_model.c:
355-
col, = i.expression.base_columns
356-
table_dict[str(i.key)] = col
357-
358-
return type(f'{db_name}DeclarativeBaseClass', (declarative_base(),), table_dict), NO_PRIMARY_KEY
359-
360-
361342
def path_query_builder(params, model) -> List[Union[BinaryExpression]]:
362343
query = []
363344
if not params:
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from fastapi import FastAPI
2+
from fastapi_quickcrud import crud_router_builder
3+
from sqlalchemy import *
4+
from sqlalchemy.orm import *
5+
from fastapi_quickcrud.crud_router import generic_sql_crud_router_builder
6+
7+
Base = declarative_base()
8+
9+
10+
class Account(Base):
11+
__tablename__ = "account"
12+
id = Column(Integer, primary_key=True, autoincrement=True)
13+
post = relationship("BlogPost", back_populates="account_f")
14+
15+
16+
class BlogPost(Base):
17+
__tablename__ = "blog_post"
18+
id = Column(Integer, primary_key=True, autoincrement=True)
19+
account_id = Column(Integer, ForeignKey("account.id"), nullable=False)
20+
account_f = relationship("Account", back_populates="post")
21+
blog = relationship("BlogComment", back_populates="post")
22+
23+
24+
class BlogComment(Base):
25+
__tablename__ = "blog_comment"
26+
id = Column(Integer, primary_key=True, autoincrement=True)
27+
blog_id = Column(Integer, ForeignKey("blog_post.id"), nullable=False)
28+
post = relationship("BlogPost", back_populates="blog")
29+
30+
31+
crud_route_parent = crud_router_builder(
32+
db_model=Account,
33+
prefix="/account",
34+
tags=["account"],
35+
foreign_include=[BlogComment, BlogPost]
36+
37+
)
38+
print("BlogPost222")
39+
40+
crud_route_child1 = generic_sql_crud_router_builder(
41+
db_model=BlogPost,
42+
prefix="/blog_post",
43+
tags=["blog_post"],
44+
foreign_include=[BlogComment, Account]
45+
46+
)
47+
print("BlogComment111")
48+
49+
crud_route_child2 = generic_sql_crud_router_builder(
50+
db_model=BlogComment,
51+
prefix="/blog_comment",
52+
tags=["blog_comment"],
53+
foreign_include=[BlogPost]
54+
55+
)
56+
57+
app = FastAPI()
58+
[app.include_router(i) for i in [crud_route_parent, crud_route_child1, crud_route_child2]]
59+
60+
61+
@app.get("/", tags=["child"])
62+
async def root():
63+
return {"message": "Hello World"}
64+
65+
66+
import uvicorn
67+
68+
uvicorn.run(app, host="0.0.0.0", port=8002, debug=False)

src/fastapi_quickcrud/test.py

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,43 @@
33
from sqlalchemy import *
44
from sqlalchemy.orm import *
55
from fastapi_quickcrud.crud_router import generic_sql_crud_router_builder
6+
from fastapi_quickcrud.misc.covert_model import convert_table_to_model
67

78
Base = declarative_base()
89

9-
class Account(Base):
10-
__tablename__ = "account"
11-
id = Column(Integer, primary_key=True, autoincrement=True)
12-
blog_post = relationship("BlogPost", back_populates="account")
13-
class BlogPost(Base):
14-
__tablename__ = "blog_post"
15-
id = Column(Integer, primary_key=True, autoincrement=True)
16-
account_id = Column(Integer, ForeignKey("account.id"), nullable=False)
17-
account = relationship("Account", back_populates="blog_post")
18-
blog_comment = relationship("BlogComment", back_populates="blog_post")
19-
20-
class BlogComment(Base):
21-
__tablename__ = "blog_comment"
22-
id = Column(Integer, primary_key=True, autoincrement=True)
23-
blog_id = Column(Integer, ForeignKey("blog_post.id"), nullable=False)
24-
blog_post = relationship("BlogPost", back_populates="blog_comment")
25-
26-
27-
crud_route_parent = crud_router_builder(
28-
db_model=Account,
29-
prefix="/account",
30-
tags=["account"],
31-
foreign_include=[BlogComment,BlogPost]
32-
33-
)
34-
print("BlogPost222")
35-
36-
crud_route_child1 = generic_sql_crud_router_builder(
37-
db_model=BlogPost,
38-
prefix="/blog_post",
39-
tags=["blog_post"],
40-
foreign_include = [BlogComment]
41-
42-
)
43-
print("BlogComment111")
44-
45-
crud_route_child2 = generic_sql_crud_router_builder(
46-
db_model=BlogComment,
47-
prefix="/blog_comment",
48-
tags=["blog_comment"]
49-
10+
association_table = Table('association', Base.metadata,
11+
Column('left_id', ForeignKey('left.id'), primary_key=True),
12+
Column('right_id', ForeignKey('right.id'), primary_key=True)
5013
)
5114

15+
class Parent(Base):
16+
__tablename__ = 'left'
17+
id = Column(Integer, primary_key=True)
18+
children = relationship(
19+
"Child",
20+
secondary=association_table,
21+
back_populates="parents")
22+
23+
class Child(Base):
24+
__tablename__ = 'right'
25+
id = Column(Integer, primary_key=True)
26+
parents = relationship(
27+
"Parent",
28+
secondary=association_table,
29+
back_populates="children")
30+
31+
crud_route_1 = crud_router_builder(db_model=Parent,
32+
prefix="/parent",
33+
tags=["Parent"],
34+
foreign_include=[Child]
35+
)
36+
crud_route_2 = crud_router_builder(db_model=Child,
37+
prefix="/child",
38+
tags=["Child"],
39+
foreign_include=[Parent]
40+
)
5241
app = FastAPI()
53-
[app.include_router(i) for i in [crud_route_parent, crud_route_child1, crud_route_child2]]
42+
[app.include_router(i) for i in [crud_route_1, crud_route_2]]
5443

5544
@app.get("/", tags=["child"])
5645
async def root():

tests/test_implementations/test_memory_sqlalchemy/api_test/foreign_tree/__init__.py

Whitespace-only changes.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import json
2+
3+
from fastapi import FastAPI
4+
from sqlalchemy import Column, Integer, \
5+
ForeignKey, Table, String
6+
from sqlalchemy.orm import declarative_base, sessionmaker, relationship
7+
8+
from src.fastapi_quickcrud.misc.type import SqlType
9+
from src.fastapi_quickcrud.crud_router import crud_router_builder
10+
11+
app = FastAPI()
12+
13+
Base = declarative_base()
14+
metadata = Base.metadata
15+
16+
from sqlalchemy import create_engine
17+
18+
from sqlalchemy.pool import StaticPool
19+
engine = create_engine('sqlite://', echo=True,
20+
connect_args={"check_same_thread": False}, pool_recycle=7200, poolclass=StaticPool)
21+
session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
22+
23+
24+
def get_transaction_session():
25+
try:
26+
db = session()
27+
yield db
28+
finally:
29+
db.close()
30+
31+
32+
class User(Base):
33+
__tablename__ = 'test_users'
34+
id = Column(Integer, primary_key=True, autoincrement=True, unique=True)
35+
name = Column(String, nullable=False)
36+
email = Column(String, nullable=False)
37+
38+
39+
friend = Table(
40+
'test_friend', Base.metadata,
41+
Column('id', ForeignKey('test_users.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False),
42+
Column('friend_name', String, nullable=False)
43+
)
44+
45+
crud_route_1 = crud_router_builder(db_session=get_transaction_session,
46+
db_model=User,
47+
prefix="/user",
48+
tags=["User"],
49+
foreign_include=[friend]
50+
)
51+
crud_route_2 = crud_router_builder(db_session=get_transaction_session,
52+
db_model=friend,
53+
prefix="/friend",
54+
tags=["friend"],
55+
foreign_include=[User]
56+
)
57+
58+
from starlette.testclient import TestClient
59+
60+
[app.include_router(i) for i in
61+
[crud_route_1,crud_route_2]]
62+
63+
client = TestClient(app)
64+
65+
66+
def test_():
67+
headers = {
68+
'accept': '*/*',
69+
'Content-Type': 'application/json',
70+
}
71+
data = '[{"id": 1,"name": "string","email": "string"}]'
72+
response = client.post('/user', headers=headers, data=data)
73+
assert response.status_code == 201
74+
assert response.json() == [{"id": 1,"name": "string","email": "string"}]
75+
76+
77+
data =' [{"id": 1,"friend_name": "string"}]'
78+
response = client.post('/friend', headers=headers, data = data)
79+
assert response.status_code == 201
80+
assert response.json() == [
81+
{
82+
"id": 1,
83+
"friend_name": "string"
84+
}
85+
]
86+
87+
response = client.get('/friend?join_foreign_table=test_users', headers=headers)
88+
assert response.status_code == 200
89+
assert response.json() == [
90+
{
91+
"test_users_foreign": [
92+
{
93+
"id": 1,
94+
"name": "string",
95+
"email": "string"
96+
}
97+
],
98+
"id": 1,
99+
"friend_name": "string"
100+
}
101+
]
102+
103+
104+
105+
def setup_module(module):
106+
User.__table__.create(module.engine, checkfirst=True)
107+
friend.create(module.engine, checkfirst=True)
108+
109+
110+
def teardown_module(module):
111+
friend.drop(engine, checkfirst=True)
112+
User.__table__.drop(engine, checkfirst=True)

0 commit comments

Comments
 (0)