Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/db/model/base.py: 100%

34 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-12-31 19:12 -0600

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# WuttJamaican -- Base package for Wutta Framework 

5# Copyright © 2023-2025 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24Base Models 

25 

26.. class:: Base 

27 

28 This is the base class for all :term:`data models <data model>` in 

29 the :term:`app database`. You should inherit from this class when 

30 defining custom models. 

31 

32 This class inherits from :class:`WuttaModelBase`. 

33""" 

34 

35import sqlalchemy as sa 

36from sqlalchemy import orm 

37from sqlalchemy.ext.associationproxy import association_proxy 

38 

39from wuttjamaican.db.util import naming_convention, ModelBase, uuid_column 

40 

41 

42class WuttaModelBase(ModelBase): # pylint: disable=too-few-public-methods 

43 """ 

44 Base class for data models, from which :class:`Base` inherits. 

45 

46 Custom models should inherit from :class:`Base` instead of this 

47 class. 

48 """ 

49 

50 @classmethod 

51 def make_proxy(cls, main_class, extension, name, proxy_name=None): 

52 """ 

53 Convenience method to declare an "association proxy" for the 

54 main class, per the params. 

55 

56 For more info see 

57 :doc:`sqlalchemy:orm/extensions/associationproxy`. 

58 

59 :param main_class: Reference to the "parent" model class, upon 

60 which the proxy will be defined. 

61 

62 :param extension: Attribute name on the main class, which 

63 references the extension record. 

64 

65 :param name: Attribute name on the extension class, which 

66 provides the proxied value. 

67 

68 :param proxy_name: Optional attribute name on the main class, 

69 which will reference the proxy. If not specified, ``name`` 

70 will be used. 

71 

72 As a simple example consider this model, which extends the 

73 :class:`~wuttjamaican.db.model.auth.User` class. In 

74 particular note the last line which is what we're documenting 

75 here:: 

76 

77 import sqlalchemy as sa 

78 from sqlalchemy import orm 

79 from wuttjamaican.db import model 

80 

81 class PoserUser(model.Base): 

82 \""" Poser extension for User \""" 

83 __tablename__ = 'poser_user' 

84 

85 uuid = model.uuid_column(sa.ForeignKey('user.uuid'), default=None) 

86 user = orm.relationship( 

87 model.User, 

88 doc="Reference to the main User record.", 

89 backref=orm.backref( 

90 '_poser', 

91 uselist=False, 

92 cascade='all, delete-orphan', 

93 doc="Reference to the Poser extension record.")) 

94 

95 favorite_color = sa.Column(sa.String(length=100), nullable=False, doc=\""" 

96 User's favorite color. 

97 \""") 

98 

99 def __str__(self): 

100 return str(self.user) 

101 

102 # nb. this is the method call 

103 PoserUser.make_proxy(model.User, '_poser', 'favorite_color') 

104 

105 That code defines a ``PoserUser`` model but also defines a 

106 ``favorite_color`` attribute on the main ``User`` class, such 

107 that it can be used normally:: 

108 

109 user = model.User(username='barney', favorite_color='green') 

110 session.add(user) 

111 

112 user = session.query(model.User).filter_by(username='bambam').one() 

113 print(user.favorite_color) 

114 """ 

115 proxy = association_proxy( 

116 extension, proxy_name or name, creator=lambda value: cls(**{name: value}) 

117 ) 

118 setattr(main_class, name, proxy) 

119 

120 

121metadata = sa.MetaData(naming_convention=naming_convention) 

122 

123Base = orm.declarative_base(metadata=metadata, cls=WuttaModelBase) 

124 

125 

126class Setting(Base): # pylint: disable=too-few-public-methods 

127 """ 

128 Represents a :term:`config setting`. 

129 """ 

130 

131 __tablename__ = "setting" 

132 

133 name = sa.Column( 

134 sa.String(length=255), 

135 primary_key=True, 

136 nullable=False, 

137 doc=""" 

138 Unique name for the setting. 

139 """, 

140 ) 

141 

142 value = sa.Column( 

143 sa.Text(), 

144 nullable=True, 

145 doc=""" 

146 String value for the setting. 

147 """, 

148 ) 

149 

150 def __str__(self): 

151 return self.name or "" 

152 

153 

154class Person(Base): 

155 """ 

156 Represents a person. 

157 

158 The use for this table in the base framework, is to associate with 

159 a :class:`~wuttjamaican.db.model.auth.User` to provide first and 

160 last name etc. (However a user does not have to be associated 

161 with any person.) 

162 

163 But this table could also be used as a basis for a Customer or 

164 Employee relationship etc. 

165 """ 

166 

167 __tablename__ = "person" 

168 __versioned__ = {} 

169 __wutta_hint__ = { 

170 "model_title": "Person", 

171 "model_title_plural": "People", 

172 } 

173 

174 uuid = uuid_column() 

175 

176 full_name = sa.Column( 

177 sa.String(length=100), 

178 nullable=False, 

179 doc=""" 

180 Full name for the person. Note that this is *required*. 

181 """, 

182 ) 

183 

184 first_name = sa.Column( 

185 sa.String(length=50), 

186 nullable=True, 

187 doc=""" 

188 The person's first name. 

189 """, 

190 ) 

191 

192 middle_name = sa.Column( 

193 sa.String(length=50), 

194 nullable=True, 

195 doc=""" 

196 The person's middle name or initial. 

197 """, 

198 ) 

199 

200 last_name = sa.Column( 

201 sa.String(length=50), 

202 nullable=True, 

203 doc=""" 

204 The person's last name. 

205 """, 

206 ) 

207 

208 users = orm.relationship( 

209 "User", 

210 back_populates="person", 

211 cascade_backrefs=False, 

212 doc=""" 

213 List of :class:`~wuttjamaican.db.model.auth.User` accounts for 

214 the person. Typically there is only one user account per 

215 person, but technically multiple are supported. 

216 """, 

217 ) 

218 

219 def __str__(self): 

220 return self.full_name or "" 

221 

222 @property 

223 def user(self): 

224 """ 

225 Reference to the "first" 

226 :class:`~wuttjamaican.db.model.auth.User` account for the 

227 person, or ``None``. 

228 

229 .. warning:: 

230 

231 Note that the database schema supports multiple users per 

232 person, but this property logic ignores that and will only 

233 ever return "one or none". That might be fine in 99% of 

234 cases, but if multiple accounts exist for a person, the one 

235 returned is indeterminate. 

236 

237 See :attr:`users` to access the full list. 

238 """ 

239 

240 # TODO: i'm not crazy about the ambiguity here re: number of 

241 # user accounts a person may have. in particular it's not 

242 # clear *which* user account would be returned, as there is no 

243 # sequence ordinal defined etc. a better approach might be to 

244 # force callers to assume the possibility of multiple 

245 # user accounts per person? (if so, remove this property) 

246 

247 if self.users: 

248 return self.users[0] 

249 return None