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

32 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-15 11:33 -0500

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

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

3# 

4# WuttJamaican -- Base package for Wutta Framework 

5# Copyright © 2023-2024 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, 

40 uuid_column, uuid_fk_column) 

41 

42 

43class WuttaModelBase(ModelBase): 

44 """ 

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

46 

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

48 class. 

49 """ 

50 

51 @classmethod 

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

53 """ 

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

55 main class, per the params. 

56 

57 For more info see 

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

59 

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

61 which the proxy will be defined. 

62 

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

64 references the extension record. 

65 

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

67 provides the proxied value. 

68 

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

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

71 will be used. 

72 

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

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

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

76 here:: 

77 

78 import sqlalchemy as sa 

79 from sqlalchemy import orm 

80 from wuttjamaican.db import model 

81 

82 class PoserUser(model.Base): 

83 \""" Poser extension for User \""" 

84 __tablename__ = 'poser_user' 

85 

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

87 user = orm.relationship( 

88 model.User, 

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

90 backref=orm.backref( 

91 '_poser', 

92 uselist=False, 

93 cascade='all, delete-orphan', 

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

95 

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

97 User's favorite color. 

98 \""") 

99 

100 def __str__(self): 

101 return str(self.user) 

102 

103 # nb. this is the method call 

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

105 

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

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

108 that it can be used normally:: 

109 

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

111 session.add(user) 

112 

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

114 print(user.favorite_color) 

115 """ 

116 proxy = association_proxy( 

117 extension, proxy_name or name, 

118 creator=lambda value: cls(**{name: value})) 

119 setattr(main_class, name, proxy) 

120 

121 

122metadata = sa.MetaData(naming_convention=naming_convention) 

123 

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

125 

126 

127class Setting(Base): 

128 """ 

129 Represents a :term:`config setting`. 

130 """ 

131 __tablename__ = 'setting' 

132 

133 name = sa.Column(sa.String(length=255), primary_key=True, nullable=False, doc=""" 

134 Unique name for the setting. 

135 """) 

136 

137 value = sa.Column(sa.Text(), nullable=True, doc=""" 

138 String value for the setting. 

139 """) 

140 

141 def __str__(self): 

142 return self.name or "" 

143 

144 

145class Person(Base): 

146 """ 

147 Represents a person. 

148 

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

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

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

152 with any person.) 

153 

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

155 Employee relationship etc. 

156 """ 

157 __tablename__ = 'person' 

158 __versioned__ = {} 

159 

160 uuid = uuid_column() 

161 

162 full_name = sa.Column(sa.String(length=100), nullable=False, doc=""" 

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

164 """) 

165 

166 first_name = sa.Column(sa.String(length=50), nullable=True, doc=""" 

167 The person's first name. 

168 """) 

169 

170 middle_name = sa.Column(sa.String(length=50), nullable=True, doc=""" 

171 The person's middle name or initial. 

172 """) 

173 

174 last_name = sa.Column(sa.String(length=50), nullable=True, doc=""" 

175 The person's last name. 

176 """) 

177 

178 users = orm.relationship( 

179 'User', 

180 back_populates='person', 

181 cascade_backrefs=False, 

182 doc=""" 

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

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

185 person, but technically multiple are supported. 

186 """) 

187 

188 def __str__(self): 

189 return self.full_name or "" 

190 

191 @property 

192 def user(self): 

193 """ 

194 Reference to the "first" 

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

196 person, or ``None``. 

197 

198 .. warning:: 

199 

200 Note that the database schema supports multiple users per 

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

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

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

204 returned is indeterminate. 

205 

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

207 """ 

208 

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

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

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

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

213 # force callers to assume the possibility of multiple 

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

215 

216 if self.users: 

217 return self.users[0]