clear-skies 2.0.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of clear-skies might be problematic. Click here for more details.
- {clear_skies-2.0.0.dist-info → clear_skies-2.0.1.dist-info}/METADATA +2 -2
- {clear_skies-2.0.0.dist-info → clear_skies-2.0.1.dist-info}/RECORD +25 -24
- clearskies/authentication/authentication.py +4 -0
- clearskies/authentication/authorization.py +4 -0
- clearskies/authentication/jwks.py +6 -1
- clearskies/authentication/secret_bearer.py +1 -1
- clearskies/backends/backend.py +13 -0
- clearskies/column.py +3 -6
- clearskies/configs/__init__.py +3 -0
- clearskies/configs/config.py +4 -1
- clearskies/configs/endpoint_list.py +28 -0
- clearskies/contexts/cli.py +4 -0
- clearskies/contexts/context.py +13 -0
- clearskies/contexts/wsgi.py +4 -0
- clearskies/contexts/wsgi_ref.py +4 -0
- clearskies/di/di.py +2 -2
- clearskies/endpoint.py +5 -4
- clearskies/endpoint_group.py +13 -0
- clearskies/endpoints/callable.py +4 -3
- clearskies/model.py +526 -30
- clearskies/query/query.py +4 -4
- clearskies/security_header.py +7 -0
- clearskies/validator.py +12 -0
- {clear_skies-2.0.0.dist-info → clear_skies-2.0.1.dist-info}/LICENSE +0 -0
- {clear_skies-2.0.0.dist-info → clear_skies-2.0.1.dist-info}/WHEEL +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: clear-skies
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: A framework for building backends in the cloud
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Conor Mancone
|
|
@@ -25,7 +25,7 @@ Requires-Dist: pymysql (>=1.1.0,<2.0.0) ; extra == "mysql"
|
|
|
25
25
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
26
26
|
Requires-Dist: typing-extensions (>=4.12.0,<5.0.0) ; python_version == "3.10"
|
|
27
27
|
Requires-Dist: wrapt (>=1.16.0,<2.0.0)
|
|
28
|
-
Project-URL: Repository, https://github.com/
|
|
28
|
+
Project-URL: Repository, https://github.com/clearskies-py/clearskies
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
31
|
# clearskies
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
clearskies/__init__.py,sha256=HLBu0CXipIp-Cr_-iBfpN4UlrFwsjTNFOwF9i0iN5i0,1241
|
|
2
2
|
clearskies/action.py,sha256=x_T05XaC0m-BYPZWaSR94XbE7T5ZK1vRtSfEszfZuDg,121
|
|
3
3
|
clearskies/authentication/__init__.py,sha256=MxXNCWhKbha7TPu_MAyssfTdOCEDSNBwzshEHx8gfHo,525
|
|
4
|
-
clearskies/authentication/authentication.py,sha256=
|
|
5
|
-
clearskies/authentication/authorization.py,sha256=
|
|
4
|
+
clearskies/authentication/authentication.py,sha256=N-g2qNDGuJaSlMwc_SRwsjAuqOMLI2nKfUzSCF7HOFo,1240
|
|
5
|
+
clearskies/authentication/authorization.py,sha256=3XEOUlQx9XeRPpNEk4GCa-xWafrz-3Xd0T1PCSFuVxY,570
|
|
6
6
|
clearskies/authentication/authorization_pass_through.py,sha256=d3XKcUWmI_u9QBBTGKrU1hdD-LM56iH60Qv_xc8TE6k,512
|
|
7
|
-
clearskies/authentication/jwks.py,sha256=
|
|
7
|
+
clearskies/authentication/jwks.py,sha256=5QVqfXsT95jEKkbm1xnUZsEoIwqMLviDyDRlK_s8xAw,4888
|
|
8
8
|
clearskies/authentication/public.py,sha256=GSNl4X4fnB25BZoXSo8nqL0iZCa9rzfvPfIsJ2Y34B4,84
|
|
9
|
-
clearskies/authentication/secret_bearer.py,sha256=
|
|
9
|
+
clearskies/authentication/secret_bearer.py,sha256=3eENEx38lTbiLkHKixE_m32RVeKt_ZTwjCc2WGMft7I,17618
|
|
10
10
|
clearskies/autodoc/__init__.py,sha256=JRUAmd0he8iGlgiZvxewLMIXJqnOFEdvlaKAtHpC2lo,124
|
|
11
11
|
clearskies/autodoc/formats/__init__.py,sha256=3rhoLKmEwT6PljaHvOl9qdeMIXyD7PQBZbqZKy5Mb5I,56
|
|
12
12
|
clearskies/autodoc/formats/oai3_json/__init__.py,sha256=C9DzR38Uu0t41osMzhQx6K-hyaNWA1t6agTzOXW9k88,142
|
|
@@ -48,11 +48,11 @@ clearskies/autodoc/schema/schema.py,sha256=KZ4r88OUMzbeV4DQ3yUhK8bHS_zJiimsWPWc1
|
|
|
48
48
|
clearskies/autodoc/schema/string.py,sha256=GXRI-aU8PYmjfnhmfaOe4vq7JASb-aB4SiFeG-EmJY0,60
|
|
49
49
|
clearskies/backends/__init__.py,sha256=krBB3gHLP-lVYkm5ksdK7teEtouAHhL3EC__WGVnF4A,3157
|
|
50
50
|
clearskies/backends/api_backend.py,sha256=fRt0U5pZn41ltZUKjbYguJrtNxchSJICLOYIV1bJV7A,54901
|
|
51
|
-
clearskies/backends/backend.py,sha256=
|
|
51
|
+
clearskies/backends/backend.py,sha256=_TFMXAHgIY-vPGhx8SYjSTbVd9r-6KpLbLnfNsCXuV8,5874
|
|
52
52
|
clearskies/backends/cursor_backend.py,sha256=lMnkHATU2Fxr0ihgrVnvI9IVnUPxNfjzpkdJIok2mXY,14360
|
|
53
53
|
clearskies/backends/memory_backend.py,sha256=fXgbFcTvbZeKHs4CsI_yNsofHSjWqZqY9QWI7uJzVws,34260
|
|
54
54
|
clearskies/backends/secrets_backend.py,sha256=zlS2ELM6pgS18tt6KvyOPqHWMMFCqsu76prEnZ8GeE0,4323
|
|
55
|
-
clearskies/column.py,sha256=
|
|
55
|
+
clearskies/column.py,sha256=4FdsU4hh1u7ekY42uh2_A5J13RtPxaf84NRj39gbTak,51963
|
|
56
56
|
clearskies/columns/__init__.py,sha256=a__-1hv-aMV3RU0Sd-BEkfItSJaG51EF2VeRQoTdka8,1990
|
|
57
57
|
clearskies/columns/audit.py,sha256=vwWQR_lNKonihb71qUoKQwzLQ5VA1ASFOm7ggA9jJ3Y,7628
|
|
58
58
|
clearskies/columns/belongs_to_id.py,sha256=H6yWsnMgr1za8hRWW40R499SVuTd11Ixb1YCacqCTHM,17820
|
|
@@ -89,7 +89,7 @@ clearskies/columns/timestamp.py,sha256=_1T20qTVHeLEqqKlxb7IFwUvcYsLWPrGMylPwvVCG
|
|
|
89
89
|
clearskies/columns/updated.py,sha256=_gJw8W_HlNKr-bHTE4eHqMqbFqJ4JcSzTRDLOt7d0Us,3481
|
|
90
90
|
clearskies/columns/uuid.py,sha256=WjWqkGHU6MlxjNQYW8lscgsOfG4mDQnJZGxEqQAoeQU,2432
|
|
91
91
|
clearskies/configs/README.md,sha256=4-r2FvkrBTrKkcKgJ5O3Qj1RZ7nnkknenKze4XYlQDs,4827
|
|
92
|
-
clearskies/configs/__init__.py,sha256=
|
|
92
|
+
clearskies/configs/__init__.py,sha256=XnnokLN7tYOXRhupmHEIVH-InTHlPJZ28kNHPhzOuLM,5093
|
|
93
93
|
clearskies/configs/actions.py,sha256=5OwrokU8uTSSQ_6SvL94SOP5JVkvRRFGAXNcCdMt8aQ,1388
|
|
94
94
|
clearskies/configs/any.py,sha256=_nac0cZY9uRhwGQZqj2N72ZFehYqOedb97lPADP3Yg8,352
|
|
95
95
|
clearskies/configs/any_dict.py,sha256=B8Gd0zwC-6UxvMvPsXMJySzVMADxG3oxnrhgBnJWCGo,871
|
|
@@ -101,10 +101,11 @@ clearskies/configs/boolean_or_callable.py,sha256=nvvWD_A5HoxW-6_rZJL23_1517T8IQ6
|
|
|
101
101
|
clearskies/configs/callable_config.py,sha256=gcTlRwCppdaj1gFuCugc4ahxMCTEDKD_z_f85eadWpc,650
|
|
102
102
|
clearskies/configs/columns.py,sha256=WhdpPUiwVqwBO2XCDDiMym2nWr_1-L1JoICyob4T6Wo,1258
|
|
103
103
|
clearskies/configs/conditions.py,sha256=a09bVac4-10ZtKPQGSu_e9VnHnwITwy_7_T4NekXpgs,1008
|
|
104
|
-
clearskies/configs/config.py,sha256=
|
|
104
|
+
clearskies/configs/config.py,sha256=Mw9wnPzEekHGfmNFH9f2vJ3BOPx3l1ktxwZc95flyQg,873
|
|
105
105
|
clearskies/configs/datetime.py,sha256=jRYXwLeTUqh8FNwWq_0yYoSH8YU9NEhYhuQScr56nTg,653
|
|
106
106
|
clearskies/configs/datetime_or_callable.py,sha256=81YCDk0g5cIxdXxgem1Ei6m7gUI213Cfryp3GyBfpD4,799
|
|
107
107
|
clearskies/configs/endpoint.py,sha256=NEDthtdPzejAot7yzUjC01PvQJR9BIyk5YnWXCi1w9I,759
|
|
108
|
+
clearskies/configs/endpoint_list.py,sha256=I_o5T46XT-I-tbQODJbpCF9LkgEccOz97mYFZuwjx4M,1144
|
|
108
109
|
clearskies/configs/float.py,sha256=3870-DkoXCyeOSwUGa69BzhG4XI_tNfei5cX-q1b1UQ,585
|
|
109
110
|
clearskies/configs/float_or_callable.py,sha256=lIOg20Xw0AOB0k3I7f0b98srVgMWQKcyM4aWRT0tUI4,708
|
|
110
111
|
clearskies/configs/integer.py,sha256=d6uoQaUoE01rrWOsxYl1vHG_raUFmB0AUOgoCWwOVC0,584
|
|
@@ -137,14 +138,14 @@ clearskies/configs/writeable_model_column.py,sha256=ZwroRcvmNznggtNMSZiAmKAvllcV
|
|
|
137
138
|
clearskies/configs/writeable_model_columns.py,sha256=D8E4aQRg1MX8-TM8lkyrWE-G_YoX2eerX4KLszgk0hA,323
|
|
138
139
|
clearskies/configurable.py,sha256=FFkKwlQk17KOLnZiTsTF3hrCu2WxRSuuddNNhz9aySY,3049
|
|
139
140
|
clearskies/contexts/__init__.py,sha256=f7XVUq2UKlDH6fjmcUWk6lbe9p_OaGpZ5ZjM6CuwTGQ,247
|
|
140
|
-
clearskies/contexts/cli.py,sha256=
|
|
141
|
-
clearskies/contexts/context.py,sha256=
|
|
142
|
-
clearskies/contexts/wsgi.py,sha256=
|
|
143
|
-
clearskies/contexts/wsgi_ref.py,sha256=
|
|
141
|
+
clearskies/contexts/cli.py,sha256=HSzHieVsD3a0uQb6FBue17HVT4PjSY8LWhjW3rw7lQw,285
|
|
142
|
+
clearskies/contexts/context.py,sha256=jbd8JdDkFruHiAYHZWrLjOvqz31LkvfApArCOVwkyfo,3630
|
|
143
|
+
clearskies/contexts/wsgi.py,sha256=lENadrI53LYUCa3Mn4JjxhTn-53Ry9l6w2kBU3CQ5AM,611
|
|
144
|
+
clearskies/contexts/wsgi_ref.py,sha256=Tkq0QYE7_ZdYvEcRKbyh-6UwwuExUadKdylwdxcCmQQ,1814
|
|
144
145
|
clearskies/di/__init__.py,sha256=uJ1NkACEIaot7ALr3M6vQ55Pe9By_s_1z52ssAnw28E,466
|
|
145
146
|
clearskies/di/additional_config.py,sha256=65INxw8aqTZQsyaKPj-aQmd6FBe4_4DwibXGgWYBy14,5139
|
|
146
147
|
clearskies/di/additional_config_auto_import.py,sha256=XYw0Kcnp6hp-ee-c0YjiATwJvRb2E82xk9PuoX9dGRY,758
|
|
147
|
-
clearskies/di/di.py,sha256=
|
|
148
|
+
clearskies/di/di.py,sha256=pWNzUDd2nxnKZP12I5SpAL3PYBWCmnM6jsT7i7cEGCM,45137
|
|
148
149
|
clearskies/di/inject/__init__.py,sha256=plEkWId-VyhvqX5KM2HhdCqi7_ZJzPmFz69cPAo812Y,643
|
|
149
150
|
clearskies/di/inject/by_class.py,sha256=dUA_ZseLZWT30wnkg272rWwe5FzUpr0gCMMQP_HFahc,796
|
|
150
151
|
clearskies/di/inject/by_name.py,sha256=oEfzeotUXPbUAiBrkE7i19nD2ISk59EEwCH3pcgSmqA,639
|
|
@@ -162,11 +163,11 @@ clearskies/di/test_module/__init__.py,sha256=7YHQF7JHP0FdI7GdEGANSZ_t1EISQYhUNm1
|
|
|
162
163
|
clearskies/di/test_module/another_module/__init__.py,sha256=8SRmHPDepLKGWTUSc1ucDF6U8mJPsNDsBDmBQCpzPWo,35
|
|
163
164
|
clearskies/di/test_module/module_class.py,sha256=I_-wnMuHfbsvti-7d2Z4bXnr6deo__uvww9nds9qrlE,46
|
|
164
165
|
clearskies/end.py,sha256=Z4C9n7OvBky_640oJ8d19jIr_ap1iqjGJqqO6s_zF5g,8919
|
|
165
|
-
clearskies/endpoint.py,sha256=
|
|
166
|
-
clearskies/endpoint_group.py,sha256=
|
|
166
|
+
clearskies/endpoint.py,sha256=ewxvcbRRJ-R1u0c39NU4yEVFDr4hvdkwgcdKD7O4m4g,48855
|
|
167
|
+
clearskies/endpoint_group.py,sha256=K1HLIMhU75ezC_SytwA-4j3ViFjjGZOBurRN5Urc6lk,11299
|
|
167
168
|
clearskies/endpoints/__init__.py,sha256=CZJQ3rHF1GA0QvH9CKxUFM1pEnJrg-2wAj5rzIDdzjc,689
|
|
168
169
|
clearskies/endpoints/advanced_search.py,sha256=c1fj81nohHGqe56t00o6kLN3XIh3kfy-aKy_C1pxwgw,21835
|
|
169
|
-
clearskies/endpoints/callable.py,sha256=
|
|
170
|
+
clearskies/endpoints/callable.py,sha256=mg1kWafU8owq_PiGHO3rr9Wl-kSRcBlnsWLb-Wd67xc,13957
|
|
170
171
|
clearskies/endpoints/create.py,sha256=Fgm8ZN2WreGTjWreSyty1c80SP8_qHcjK_be9hpWELw,8757
|
|
171
172
|
clearskies/endpoints/delete.py,sha256=GWnD5-By_5clJzDi8Mac5aeL7HArDpRet3Qo7GPinWk,5296
|
|
172
173
|
clearskies/endpoints/get.py,sha256=w-NWyq9GcQqyBHGS-dTqRdaXRXdvooD-iZ_doDbemNA,10382
|
|
@@ -198,13 +199,13 @@ clearskies/input_outputs/input_output.py,sha256=W5h8a9XFEqwi3h6v17okblz8sOozUj-m
|
|
|
198
199
|
clearskies/input_outputs/programmatic.py,sha256=gfsNFIfiF5cvxi1aS9SgArcKYUgkL5gyxjCMQLwgivY,1711
|
|
199
200
|
clearskies/input_outputs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
200
201
|
clearskies/input_outputs/wsgi.py,sha256=94tgWxiwpwvJA9G4nG-hCHteg0pq9pIDCVhnheZuAQc,2544
|
|
201
|
-
clearskies/model.py,sha256=
|
|
202
|
+
clearskies/model.py,sha256=Pt1DDU5K9okrygJ7_dsRlYf4bRosrtiAVOffk536-Rg,47259
|
|
202
203
|
clearskies/parameters_to_properties.py,sha256=kbU9Ln2mU3XX4p-QNepGgS4HiQ1BUuJnhx9T6kgB9jw,1004
|
|
203
204
|
clearskies/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
204
205
|
clearskies/query/__init__.py,sha256=ISF80_cG3rd574sRTdKKPxAdlSjtQh_ClXKKs_MSSSo,277
|
|
205
206
|
clearskies/query/condition.py,sha256=9RJrYya1Kfzx0L_BMDKQhYkN9dB7NAq5kRK5mz51YdY,8624
|
|
206
207
|
clearskies/query/join.py,sha256=4lrDUQzck7klKY_VYkc4SVK95SVwyy3SVTvasnsAEyc,4713
|
|
207
|
-
clearskies/query/query.py,sha256=
|
|
208
|
+
clearskies/query/query.py,sha256=t9tV67CeI6I1u5Uhu6KPT_qiFjvLc3GnwVM2acbXuAU,6088
|
|
208
209
|
clearskies/query/sort.py,sha256=c-EtIkjg3kLjwSTdXD7sfyx-mNUhAepUV-2izprh3iY,754
|
|
209
210
|
clearskies/schema.py,sha256=9S0RuFBiydsDy4kATy2BBsXJ9aWHILW0ja_q9F4kxb0,2872
|
|
210
211
|
clearskies/secrets/__init__.py,sha256=WauemayzKCr-guvMu6wCL4hQUIbkDJFuNKEm8Rd6cUs,136
|
|
@@ -215,7 +216,7 @@ clearskies/secrets/akeyless.py,sha256=IUI4RYD-XdNQvyhpuElnw_O74ZY4tJUUtCHmd0dArU
|
|
|
215
216
|
clearskies/secrets/exceptions/__init__.py,sha256=j-SLHD-DL0CT4cZXibD9kXHk63JEl_UKX6xL_nq1EfE,32
|
|
216
217
|
clearskies/secrets/exceptions/not_found.py,sha256=_lZwovDrd18dUHDop5pF4mhexBPNr126xF2gOLA2-EA,36
|
|
217
218
|
clearskies/secrets/secrets.py,sha256=TYN0T7XjmvETtSacDKEkyGGO9kDFCoPl5A7FNkvyNj4,1593
|
|
218
|
-
clearskies/security_header.py,sha256=
|
|
219
|
+
clearskies/security_header.py,sha256=suV3PMGFFJzMIsfDiT4t1sArkLVGzepyoftY0ddBWkM,431
|
|
219
220
|
clearskies/security_headers/__init__.py,sha256=JUpc4Y8dNBinDQAkP7OOABR7N787-lRR23boSWmY6Us,285
|
|
220
221
|
clearskies/security_headers/cache_control.py,sha256=1gxASHq74Azpm_19kDFEyT55KuO7gYeyauuvTAZVeaA,2279
|
|
221
222
|
clearskies/security_headers/cors.py,sha256=8cErvdprnT3IlBaoENbNz43UwFpaUWI_NXtJJaYEH3c,1859
|
|
@@ -225,7 +226,7 @@ clearskies/security_headers/x_content_type_options.py,sha256=47DEQpj8HBSa-_TImW-
|
|
|
225
226
|
clearskies/security_headers/x_frame_options.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
226
227
|
clearskies/test_base.py,sha256=Nw6qwsPMOx-QT_6wXn6f9P4BcA7OeGrs83IC2naNMYY,205
|
|
227
228
|
clearskies/typing.py,sha256=Kme9es5c_lw4qQCbCL7lKXtDemVl0_qBy7jxKIu4yyg,378
|
|
228
|
-
clearskies/validator.py,sha256=
|
|
229
|
+
clearskies/validator.py,sha256=nGFwe74LMJnEO7r2mgwd_V25bLpxUDH7IMolm-zk-3w,1332
|
|
229
230
|
clearskies/validators/__init__.py,sha256=Hz0L5Y7EkeK4Px6WcdQ3ku6VqUGpLHX0ZWwCpT1ts1U,1248
|
|
230
231
|
clearskies/validators/after_column.py,sha256=WPbto20CEYaKjjQtO-0t1s5DvTKW7ipDMNYaqNlr1zY,2394
|
|
231
232
|
clearskies/validators/before_column.py,sha256=fDXhgPwGvnjk1Xy6iwebE8q--BeD48uu3cVGpRGTSBc,528
|
|
@@ -242,7 +243,7 @@ clearskies/validators/minimum_value.py,sha256=Vz0e_9U5KYqBGqfrVBOtPiVtmNPWwFEGPA
|
|
|
242
243
|
clearskies/validators/required.py,sha256=gNKEZsYP04PQZc0bE_EpgFw4dWrcKnml2D2QthuD7lY,1410
|
|
243
244
|
clearskies/validators/timedelta.py,sha256=TLtFs4KhOmJpDdrf9mmsSiLtt8yz-i0ym30Cw4EcykY,1960
|
|
244
245
|
clearskies/validators/unique.py,sha256=m59azxUecp7t3WFCehcmQ6E1ZpyXdcLQ1KOSm_p5Hn4,1077
|
|
245
|
-
clear_skies-2.0.
|
|
246
|
-
clear_skies-2.0.
|
|
247
|
-
clear_skies-2.0.
|
|
248
|
-
clear_skies-2.0.
|
|
246
|
+
clear_skies-2.0.1.dist-info/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
|
|
247
|
+
clear_skies-2.0.1.dist-info/METADATA,sha256=vihY3StAUiiXlBvo2d81PEbB0po4THFpN_FFz6sewPw,1762
|
|
248
|
+
clear_skies-2.0.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
249
|
+
clear_skies-2.0.1.dist-info/RECORD,,
|
|
@@ -10,8 +10,13 @@ from clearskies.security_headers.cors import Cors
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Jwks(Authentication, clearskies.di.InjectableProperties):
|
|
13
|
-
"""
|
|
13
|
+
"""
|
|
14
|
+
Validate a JWT against a JWKS (JSON Web Key Set)
|
|
15
|
+
"""
|
|
14
16
|
|
|
17
|
+
"""
|
|
18
|
+
The URL of the JWKS
|
|
19
|
+
"""
|
|
15
20
|
jwks_url = clearskies.configs.String(required=True)
|
|
16
21
|
|
|
17
22
|
"""
|
|
@@ -495,7 +495,7 @@ class SecretBearer(Authentication, clearskies.di.InjectableProperties):
|
|
|
495
495
|
if not self._alternate_secret:
|
|
496
496
|
self._alternate_secret = (
|
|
497
497
|
self.secrets.get(self.alternate_secret_key)
|
|
498
|
-
if self.
|
|
498
|
+
if self.alternate_secret_key
|
|
499
499
|
else self.environment.get(self.alternate_environment_key)
|
|
500
500
|
)
|
|
501
501
|
return self._alternate_secret
|
clearskies/backends/backend.py
CHANGED
|
@@ -9,6 +9,19 @@ from clearskies.autodoc.schema import Schema as AutoDocSchema
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Backend(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Conecting models to their data since 2020!
|
|
14
|
+
|
|
15
|
+
The backend system acts as a flexible layer between models and their data sources. By changing the backend attached to a model,
|
|
16
|
+
you change where the model fetches and saves data. This might be a database, an in-memory data store, a dynamodb table,
|
|
17
|
+
an API, and more. This allows you to interact with a variety of data sources with the models acting as a standardized API.
|
|
18
|
+
Since endpoints also rely on the models for their functionality, this means that you can easily build API endpoints and
|
|
19
|
+
more for a variety of data sources with a minimal amount of code.
|
|
20
|
+
|
|
21
|
+
Of course, not all data sources support all functionality present in the model. Therefore, you do still need to have
|
|
22
|
+
a fair understanding of how your data sources work.
|
|
23
|
+
"""
|
|
24
|
+
|
|
12
25
|
supports_n_plus_one = False
|
|
13
26
|
can_count = True
|
|
14
27
|
|
clearskies/column.py
CHANGED
|
@@ -24,13 +24,10 @@ if TYPE_CHECKING:
|
|
|
24
24
|
|
|
25
25
|
class Column(clearskies.configurable.Configurable, clearskies.di.InjectableProperties):
|
|
26
26
|
"""
|
|
27
|
-
|
|
27
|
+
Columns are used to build schemes and enable a variety of levels of automation with clearskies.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
clearskies framework to automate things like input validation, front-end/backend-transformations, and more.
|
|
32
|
-
Many column classes have their own configuration settings, but there are also some common configuration settings
|
|
33
|
-
defined here.
|
|
29
|
+
Columns are used to define your schemas in clearskies, especially via models. The column definitions are then used by endpoints
|
|
30
|
+
and other aspects of the clearskies framework to automate things like input validation, front-end/backend-transformations, and more.
|
|
34
31
|
"""
|
|
35
32
|
|
|
36
33
|
"""
|
clearskies/configs/__init__.py
CHANGED
|
@@ -80,6 +80,7 @@ from .config import Config
|
|
|
80
80
|
from .datetime import Datetime
|
|
81
81
|
from .datetime_or_callable import DatetimeOrCallable
|
|
82
82
|
from .endpoint import Endpoint
|
|
83
|
+
from .endpoint_list import EndpointList
|
|
83
84
|
from .float import Float
|
|
84
85
|
from .float_or_callable import FloatOrCallable
|
|
85
86
|
from .integer import Integer
|
|
@@ -126,6 +127,8 @@ __all__ = [
|
|
|
126
127
|
"Config",
|
|
127
128
|
"Datetime",
|
|
128
129
|
"DatetimeOrCallable",
|
|
130
|
+
"Endpoint",
|
|
131
|
+
"EndpointList",
|
|
129
132
|
"Float",
|
|
130
133
|
"FloatOrCallable",
|
|
131
134
|
"Joins",
|
clearskies/configs/config.py
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from clearskies.configs import config
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from clearskies.endpoint import Endpoint as EndpointBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EndpointList(config.Config):
|
|
12
|
+
def __set__(self, instance, value: list[EndpointBase]):
|
|
13
|
+
if not isinstance(value, list):
|
|
14
|
+
raise TypeError(
|
|
15
|
+
f"{error_prefix} attempt to set a value of type '{value.__class__.__name__}' to a parameter that requries a list of endpoints."
|
|
16
|
+
)
|
|
17
|
+
for index, item in enumerate(value):
|
|
18
|
+
if not hasattr(item, "top_level_authentication_and_authorization"):
|
|
19
|
+
error_prefix = self._error_prefix(instance)
|
|
20
|
+
raise TypeError(
|
|
21
|
+
f"{error_prefix} attempt to set a value of type '{item.__class__.__name__}' for item #{index+1} when all items in the list should be instances of clearskies.End."
|
|
22
|
+
)
|
|
23
|
+
instance._set_config(self, value)
|
|
24
|
+
|
|
25
|
+
def __get__(self, instance, parent) -> list[EndpointBase]:
|
|
26
|
+
if not instance:
|
|
27
|
+
return self # type: ignore
|
|
28
|
+
return instance._get_config(self)
|
clearskies/contexts/cli.py
CHANGED
clearskies/contexts/context.py
CHANGED
|
@@ -15,8 +15,21 @@ if TYPE_CHECKING:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class Context:
|
|
18
|
+
"""
|
|
19
|
+
Context: a flexible way to connect applications to hosting strategies.
|
|
20
|
+
"""
|
|
21
|
+
|
|
18
22
|
di: Di = None # type: ignore
|
|
19
23
|
|
|
24
|
+
"""
|
|
25
|
+
The application to execute.
|
|
26
|
+
|
|
27
|
+
This can be a callable, an endpoint, or an endpoint group. If passed a callable, the callable can request any
|
|
28
|
+
standard or defined dependencies and should return the desired response. It can also raise any exception from
|
|
29
|
+
clearskies.exceptions.
|
|
30
|
+
"""
|
|
31
|
+
application: Callable | clearskies.endpoint.Endpoint | clearskies.endpoint_group.EndpointGroup = None # type: ignore
|
|
32
|
+
|
|
20
33
|
def __init__(
|
|
21
34
|
self,
|
|
22
35
|
application: Callable | clearskies.endpoint.Endpoint | clearskies.endpoint_group.EndpointGroup,
|
clearskies/contexts/wsgi.py
CHANGED
|
@@ -12,5 +12,9 @@ from clearskies.input_outputs import Wsgi as WsgiInputOutput
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Wsgi(Context):
|
|
15
|
+
"""
|
|
16
|
+
Connect your application to a WSGI server.
|
|
17
|
+
"""
|
|
18
|
+
|
|
15
19
|
def __call__(self, env, start_response): # type: ignore
|
|
16
20
|
return self.execute_application(WsgiInputOutput(env, start_response))
|
clearskies/contexts/wsgi_ref.py
CHANGED
clearskies/di/di.py
CHANGED
|
@@ -23,7 +23,7 @@ class Di:
|
|
|
23
23
|
Build a dependency injection object.
|
|
24
24
|
|
|
25
25
|
The dependency injection (DI) container is a key part of clearskies, so understanding how to both configure
|
|
26
|
-
them and get dependencies for your classes is important. Note however that
|
|
26
|
+
them and get dependencies for your classes is important. Note however that you don't often have
|
|
27
27
|
to interact with the dependency injection container directly. All of the configuration options for
|
|
28
28
|
the DI container are also available to all the contexts, which is typically how you will build clearskies
|
|
29
29
|
applications. So, while you can create a DI container and use it directly, typically you'll just follow
|
|
@@ -648,7 +648,7 @@ class Di:
|
|
|
648
648
|
its name and type-hint.
|
|
649
649
|
"""
|
|
650
650
|
built_value = self.build_class_from_type_hint(argument_name, type_hint, context=context, cache=True)
|
|
651
|
-
if built_value:
|
|
651
|
+
if built_value is not None:
|
|
652
652
|
return built_value
|
|
653
653
|
return self.build_from_name(argument_name, context=context, cache=True)
|
|
654
654
|
|
clearskies/endpoint.py
CHANGED
|
@@ -32,15 +32,16 @@ class Endpoint(
|
|
|
32
32
|
clearskies.di.InjectableProperties,
|
|
33
33
|
):
|
|
34
34
|
"""
|
|
35
|
-
|
|
35
|
+
Automating drudgery!
|
|
36
36
|
|
|
37
37
|
With clearskies, endpoints exist to offload some drudgery and make your life easier, but they can also
|
|
38
38
|
get out of your way when you don't need them. Think of them as pre-built endpoints that can execute
|
|
39
39
|
common functionality needed for web applications/APIs. Instead of defining a function that fetches
|
|
40
40
|
records from your backend and returns them to the end user, you can let the list endpoint do this for you
|
|
41
41
|
with a minimal amount of configuration. Instead of making an endpoint that creates records, just deploy
|
|
42
|
-
a create endpoint.
|
|
43
|
-
|
|
42
|
+
a create endpoint. While this gives clearskies some helpful capabiltiies for automation, it also has
|
|
43
|
+
the Callable endpoint which simply calls a developer-defined function, and therefore allows clearskies to
|
|
44
|
+
act like a much more typical framework.
|
|
44
45
|
"""
|
|
45
46
|
|
|
46
47
|
"""
|
|
@@ -342,7 +343,7 @@ class Endpoint(
|
|
|
342
343
|
"""
|
|
343
344
|
The model class used by this endpoint.
|
|
344
345
|
|
|
345
|
-
The
|
|
346
|
+
The endpoint will use this to fetch/save/validate incoming data as needed.
|
|
346
347
|
"""
|
|
347
348
|
model_class = clearskies.configs.ModelClass(default=None)
|
|
348
349
|
|
clearskies/endpoint_group.py
CHANGED
|
@@ -217,11 +217,24 @@ class EndpointGroup(
|
|
|
217
217
|
The dependency injection container
|
|
218
218
|
"""
|
|
219
219
|
di = clearskies.di.inject.Di()
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
The base URL for the endpoint group.
|
|
223
|
+
|
|
224
|
+
This URL is added as a prefix to all endpoints attached to the group. This includes any named URL parameters:
|
|
225
|
+
"""
|
|
220
226
|
url = clearskies.configs.String(default="")
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
The list of endpoints connected to this endpoint group
|
|
230
|
+
"""
|
|
231
|
+
endpoints = clearskies.configs.EndpointList()
|
|
232
|
+
|
|
221
233
|
response_headers = clearskies.configs.StringListOrCallable(default=[])
|
|
222
234
|
authentication = clearskies.configs.Authentication(default=Public())
|
|
223
235
|
authorization = clearskies.configs.Authorization(default=Authorization())
|
|
224
236
|
security_headers = clearskies.configs.SecurityHeaders(default=[])
|
|
237
|
+
|
|
225
238
|
cors_header: SecurityHeader = None # type: ignore
|
|
226
239
|
has_cors: bool = False
|
|
227
240
|
endpoints_initialized = False
|
clearskies/endpoints/callable.py
CHANGED
|
@@ -23,13 +23,13 @@ class Callable(Endpoint):
|
|
|
23
23
|
"""
|
|
24
24
|
An endpoint that executes a user-defined function.
|
|
25
25
|
|
|
26
|
-
The Callable endpoint does exactly that - you provide a function that will be called when the
|
|
27
|
-
all callables invoked by clearskies, you can request any defined
|
|
26
|
+
The Callable endpoint does exactly that - you provide a function that will be called when the endpoint is invoked. Like
|
|
27
|
+
all callables invoked by clearskies, you can request any defined dependency that can be provided by the clearskies
|
|
28
28
|
framework.
|
|
29
29
|
|
|
30
30
|
Whatever you return will be returned to the client. By default, the return value is sent along in the `data` parameter
|
|
31
31
|
of the standard clearskies response. To suppress this behavior, set `return_standard_response` to `False`. You can also
|
|
32
|
-
return
|
|
32
|
+
return a model instance, a model query, or a list of model instances and the callable endpoint will automatically return
|
|
33
33
|
the columns specified in `readable_column_names` to the client.
|
|
34
34
|
|
|
35
35
|
Here's a basic working example:
|
|
@@ -300,6 +300,7 @@ class Callable(Endpoint):
|
|
|
300
300
|
converted_models,
|
|
301
301
|
number_results=len(response) if response.backend.can_count else None,
|
|
302
302
|
next_page=response.next_page_data(),
|
|
303
|
+
limit=response.get_query().limit,
|
|
303
304
|
)
|
|
304
305
|
|
|
305
306
|
# or did they return a list of models?
|
clearskies/model.py
CHANGED
|
@@ -19,13 +19,166 @@ class Model(Schema, InjectableProperties):
|
|
|
19
19
|
"""
|
|
20
20
|
A clearskies model.
|
|
21
21
|
|
|
22
|
-
To be useable, a model class needs
|
|
22
|
+
To be useable, a model class needs four things:
|
|
23
23
|
|
|
24
|
-
1.
|
|
25
|
-
2.
|
|
26
|
-
3. A
|
|
27
|
-
4.
|
|
24
|
+
1. The name of the id column
|
|
25
|
+
2. A backend
|
|
26
|
+
3. A destination name (equivalent to a table name for SQL backends)
|
|
27
|
+
4. Columns
|
|
28
28
|
|
|
29
|
+
In more detail:
|
|
30
|
+
|
|
31
|
+
### Id Column Name
|
|
32
|
+
|
|
33
|
+
clearskies assumes that all models have a column that uniquely identifies each record. This id column is
|
|
34
|
+
provided where appropriate in the lifecycle of the model save process to help connect and find related records.
|
|
35
|
+
It's defined as a simple class attribute called `id_column_name`. There **MUST** be a column with the same name
|
|
36
|
+
in the column definitions. A simple approach to take is to use the Uuid column as an id column. This will
|
|
37
|
+
automatically provide a random UUID when the record is first created. If you are using auto-incrementing integers,
|
|
38
|
+
you can simply use an `Int` column type and define the column as auto-incrementing in your database.
|
|
39
|
+
|
|
40
|
+
### Backend
|
|
41
|
+
|
|
42
|
+
Every model needs a backend, which is an object that extends clearskies.Backend and is attached to the
|
|
43
|
+
`backend` attribute of the model class. clearskies comes with a variety of backends in the `clearskies.backends`
|
|
44
|
+
module that you can use, and you can also define your own or import more from additional packages.
|
|
45
|
+
|
|
46
|
+
### Destination Name
|
|
47
|
+
|
|
48
|
+
The destination name is the equivalent of a table name in other frameworks, but the name is more generic to
|
|
49
|
+
reflect the fact that clearskies is intended to work with a variety of backends - not just SQL databases.
|
|
50
|
+
The exact meaning of the destination name depends on the backend: for a cursor backend it is in fact used
|
|
51
|
+
as the table name when fetching/storing records. For the API backend it is frequently appended to a base
|
|
52
|
+
URL to reach the corect endpoint.
|
|
53
|
+
|
|
54
|
+
This is provided by a class function call `destination_name`. The base model class declares a generic method
|
|
55
|
+
for this which takes the class name, converts it from title case to snake case, and makes it plural. Hence,
|
|
56
|
+
a model class called `User` will have a default destination name of `users` and a model class of `OrderProduct`
|
|
57
|
+
will have a default destination name of `order_products`. Of course, this system isn't pefect: your backend
|
|
58
|
+
may have a different convention or you may have one of the many words in the english language that are
|
|
59
|
+
exceptions to the grammatical rules of making words plural. In this case you can simply extend the method
|
|
60
|
+
and change it according to your needs, e.g.:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
from typing import Self
|
|
64
|
+
import clearskies
|
|
65
|
+
|
|
66
|
+
class Fish(clearskies.Model):
|
|
67
|
+
@classmethod
|
|
68
|
+
def destination_name(cls: type[Self]) -> str:
|
|
69
|
+
return "fish"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Columns
|
|
73
|
+
|
|
74
|
+
Finally, columns are defined by attaching attributes to your model class that extend clearskies.Column. A variety
|
|
75
|
+
are provided by default in the clearskies.columns module, and you can always create more or import them from
|
|
76
|
+
other packages.
|
|
77
|
+
|
|
78
|
+
### Fetching From the Di Container
|
|
79
|
+
|
|
80
|
+
In order to use a model in your application you need to retrieve it from the dependency injection system. Like
|
|
81
|
+
everything, you can do this by either the name or with type hinting. Models do have a special rule for
|
|
82
|
+
injection-via-name: like all classes their dependency injection name is made by converting the class name from
|
|
83
|
+
title case to snake case, but they are also available via the pluralized name. Here's a quick example of all
|
|
84
|
+
three approaches for dependency injection:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
import clearskies
|
|
88
|
+
|
|
89
|
+
class User(clearskies.Model):
|
|
90
|
+
id_column_name = "id"
|
|
91
|
+
backend = clearskies.backends.MemoryBackend()
|
|
92
|
+
|
|
93
|
+
id = clearskies.columns.Uuid()
|
|
94
|
+
name = clearskies.columns.String()
|
|
95
|
+
|
|
96
|
+
def my_application(user, users, by_type_hint: User):
|
|
97
|
+
return {
|
|
98
|
+
"all_are_user_models": isinstance(user, User) and isinstance(users, User) and isinstance(by_type_hint, User)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
cli = clearskies.contexts.Cli(my_application, classes=[User])
|
|
102
|
+
cli()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Note that the `User` model class was provided in the `classes` list sent to the context: that's important as it
|
|
106
|
+
informs the dependency injection system that this is a class we want to provide. It's common (but not required)
|
|
107
|
+
to put all models for a clearskies application in their own separate python module and then provide those to
|
|
108
|
+
the depedency injection system via the `modules` argument to the context. So you may have a directory structure
|
|
109
|
+
like this:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
├── app/
|
|
113
|
+
│ └── models/
|
|
114
|
+
│ ├── __init__.py
|
|
115
|
+
│ ├── category.py
|
|
116
|
+
│ ├── order.py
|
|
117
|
+
│ ├── product.py
|
|
118
|
+
│ ├── status.py
|
|
119
|
+
│ └── user.py
|
|
120
|
+
└── api.py
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Where `__init__.py` imports all the models:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
from app.models.category import Category
|
|
127
|
+
from app.models.order import Order
|
|
128
|
+
from app.models.proudct import Product
|
|
129
|
+
from app.models.status import Status
|
|
130
|
+
from app.models.user import User
|
|
131
|
+
|
|
132
|
+
__all__ = ["Category", "Order", "Product", "Status", "User"]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Then in your main application you can just import the whole `models` module into your context:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
import app.models
|
|
139
|
+
|
|
140
|
+
cli = clearskies.contexts.cli(SomeApplication, modules=[app.models])
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Adding Dependencies
|
|
144
|
+
|
|
145
|
+
The base model class extends `clearskies.di.InjectableProperties` which means that you can inject dependencies into your model
|
|
146
|
+
using the `di.inject` classes. Here's an example that demonstrates dependency injection for models:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
import datetime
|
|
150
|
+
import clearskies
|
|
151
|
+
|
|
152
|
+
class SomeClass:
|
|
153
|
+
# Since this will be built by the DI system directly, we can declare dependencies in the __init__
|
|
154
|
+
def __init__(self, some_date):
|
|
155
|
+
self.some_date = some_date
|
|
156
|
+
|
|
157
|
+
class User(clearskies.Model):
|
|
158
|
+
id_column_name = "id"
|
|
159
|
+
backend = clearskies.backends.MemoryBackend()
|
|
160
|
+
|
|
161
|
+
utcnow = clearskies.di.inject.Utcnow()
|
|
162
|
+
some_class = clearskies.di.inject.ByClass(SomeClass)
|
|
163
|
+
|
|
164
|
+
id = clearskies.columns.Uuid()
|
|
165
|
+
name = clearskies.columns.String()
|
|
166
|
+
|
|
167
|
+
def some_date_in_the_past(self):
|
|
168
|
+
return self.some_class.some_date < self.utcnow
|
|
169
|
+
|
|
170
|
+
def my_application(user):
|
|
171
|
+
return user.some_date_in_the_past()
|
|
172
|
+
|
|
173
|
+
cli = clearskies.contexts.Cli(
|
|
174
|
+
my_application,
|
|
175
|
+
classes=[User],
|
|
176
|
+
bindings={
|
|
177
|
+
"some_date": datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=1),
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
cli()
|
|
181
|
+
```
|
|
29
182
|
"""
|
|
30
183
|
|
|
31
184
|
_previous_data: dict[str, Any] = {}
|
|
@@ -240,10 +393,12 @@ class Model(Schema, InjectableProperties):
|
|
|
240
393
|
return not columns[key].values_match(old_value, new_value)
|
|
241
394
|
|
|
242
395
|
def previous_value(self: Self, key: str):
|
|
396
|
+
"""Return the value of a column from before the most recent save."""
|
|
243
397
|
self.no_queries()
|
|
244
398
|
return getattr(self.__class__, key).transform(self._previous_data.get(key))
|
|
245
399
|
|
|
246
400
|
def delete(self: Self, except_if_not_exists=True) -> bool:
|
|
401
|
+
"""Delete a record."""
|
|
247
402
|
self.no_queries()
|
|
248
403
|
if not self:
|
|
249
404
|
if except_if_not_exists:
|
|
@@ -454,30 +609,154 @@ class Model(Schema, InjectableProperties):
|
|
|
454
609
|
|
|
455
610
|
def where(self: Self, where: str | Condition) -> Self:
|
|
456
611
|
"""
|
|
457
|
-
Add
|
|
612
|
+
Add a condition to a query.
|
|
613
|
+
|
|
614
|
+
The `where` method (in combination with the `find` method) is typically the starting point for query records in
|
|
615
|
+
a model. You don't *have* to add a condition to a model in order to fetch records, but of course it's a very
|
|
616
|
+
common use case. Conditions in clearskies can be built from the columns or can be constructed as SQL-like
|
|
617
|
+
string conditions, e.g. `model.where("name=Bob")` or `model.where(model.name.equals("Bob"))`. The latter
|
|
618
|
+
provides strict type-checking, while the former does not. Either way they have the same result. The list of
|
|
619
|
+
supported operators for a given column can be seen by checking the `_allowed_search_operators` attribute of the
|
|
620
|
+
column class. Most columns accept all allowed operators, which are:
|
|
621
|
+
|
|
622
|
+
- "<=>"
|
|
623
|
+
- "!="
|
|
624
|
+
- "<="
|
|
625
|
+
- ">="
|
|
626
|
+
- ">"
|
|
627
|
+
- "<"
|
|
628
|
+
- "="
|
|
629
|
+
- "in"
|
|
630
|
+
- "is not null"
|
|
631
|
+
- "is null"
|
|
632
|
+
- "like"
|
|
633
|
+
|
|
634
|
+
When working with string conditions, it is safe to inject user input into the condition. The allowed
|
|
635
|
+
format for conditions is very simple: `f"{column_name}\\s?{operator}\\s?{value}"`. This makes it possible to
|
|
636
|
+
unambiguously separate all three pieces from eachother. It's not possible to inject malicious payloads into either
|
|
637
|
+
the column names or operators because both are checked against a strict allow list (e.g. the columns declared in the
|
|
638
|
+
model or the list of allowed operators above). The value is then extracted from the leftovers, and this is
|
|
639
|
+
provided to the backend separately so it can use it appropriately (e.g. using prepared statements for the cursor
|
|
640
|
+
backend). Of course, you generally shouldn't have to inject user input into conditions very often because, most
|
|
641
|
+
often, the various list/search endpoints do this for you, but if you have to do it there are no security
|
|
642
|
+
concerns.
|
|
643
|
+
|
|
644
|
+
You can include a table name before the column name, with the two separated by a period. As always, if you do this,
|
|
645
|
+
ensure that you include a supporting join statement (via the `join` method - see it for examples).
|
|
646
|
+
|
|
647
|
+
When you call the `where` method it returns a new model object with it's query configured to include the additional
|
|
648
|
+
condition. The original model object remains unchanged. Multiple conditions are always joined with AND. There is
|
|
649
|
+
no explicit option for OR. The closest is using an IN condition.
|
|
650
|
+
|
|
651
|
+
To access the results you have to iterate over the resulting model. If you are only expecting one result
|
|
652
|
+
and want to work directly with it, then you can use `model.find(condition)` or `model.where(condition).first()`.
|
|
653
|
+
|
|
654
|
+
Example:
|
|
655
|
+
```python
|
|
656
|
+
import clearskies
|
|
458
657
|
|
|
459
|
-
|
|
658
|
+
class Order(clearskies.Model):
|
|
659
|
+
id_column_name = "id"
|
|
660
|
+
backend = clearskies.backends.MemoryBackend()
|
|
460
661
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
662
|
+
id = clearskies.columns.Uuid()
|
|
663
|
+
user_id = clearskies.columns.String()
|
|
664
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
665
|
+
total = clearskies.columns.Float()
|
|
464
666
|
|
|
465
|
-
|
|
466
|
-
|
|
667
|
+
def my_application(orders):
|
|
668
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
669
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
670
|
+
orders.create({"user_id": "Jane", "status": "Pending", "total": 30})
|
|
467
671
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
672
|
+
return [order.user_id for order in orders.where("status=Pending").where(Order.total.greater_than(25))]
|
|
673
|
+
|
|
674
|
+
cli = clearskies.contexts.Cli(
|
|
675
|
+
my_application,
|
|
676
|
+
classes=[Order],
|
|
677
|
+
)
|
|
678
|
+
cli()
|
|
474
679
|
```
|
|
680
|
+
|
|
681
|
+
Which, if ran, returns: `["Jane"]`
|
|
682
|
+
|
|
475
683
|
"""
|
|
476
684
|
self.no_single_model()
|
|
477
685
|
return self.with_query(self.get_query().add_where(where if isinstance(where, Condition) else Condition(where)))
|
|
478
686
|
|
|
479
687
|
def join(self: Self, join: str) -> Self:
|
|
480
|
-
"""
|
|
688
|
+
"""
|
|
689
|
+
Add a join clause to the query.
|
|
690
|
+
|
|
691
|
+
As with the `where` method, this expects a string which is parsed accordingly. The syntax is not as flexible as
|
|
692
|
+
SQL and expects a format of:
|
|
693
|
+
|
|
694
|
+
```
|
|
695
|
+
[left|right|inner]? join [right_table_name] ON [right_table_name].[right_column_name]=[left_table_name].[left_column_name].
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
This is case insensitive. Aliases are allowed. If you don't specify a join type it defaults to inner.
|
|
699
|
+
Here are two examples of valid join statements:
|
|
700
|
+
|
|
701
|
+
- `join orders on orders.user_id=users.id`
|
|
702
|
+
- `left join user_orders as orders on orders.id=users.id`
|
|
703
|
+
|
|
704
|
+
Note that joins are not strictly limited to SQL-like backends, but of course no all backends will support joining.
|
|
705
|
+
|
|
706
|
+
A basic example:
|
|
707
|
+
|
|
708
|
+
```
|
|
709
|
+
import clearskies
|
|
710
|
+
|
|
711
|
+
class User(clearskies.Model):
|
|
712
|
+
id_column_name = "id"
|
|
713
|
+
backend = clearskies.backends.MemoryBackend()
|
|
714
|
+
|
|
715
|
+
id = clearskies.columns.Uuid()
|
|
716
|
+
name = clearskies.columns.String()
|
|
717
|
+
|
|
718
|
+
class Order(clearskies.Model):
|
|
719
|
+
id_column_name = "id"
|
|
720
|
+
backend = clearskies.backends.MemoryBackend()
|
|
721
|
+
|
|
722
|
+
id = clearskies.columns.Uuid()
|
|
723
|
+
user_id = clearskies.columns.BelongsToId(User, readable_parent_columns=["id", "name"])
|
|
724
|
+
user = clearskies.columns.BelongsToModel("user_id")
|
|
725
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
726
|
+
total = clearskies.columns.Float()
|
|
727
|
+
|
|
728
|
+
def my_application(users, orders):
|
|
729
|
+
jane = users.create({"name": "Jane"})
|
|
730
|
+
another_jane = users.create({"name": "Jane"})
|
|
731
|
+
bob = users.create({"name": "Bob"})
|
|
732
|
+
|
|
733
|
+
# Jane's orders
|
|
734
|
+
orders.create({"user_id": jane.id, "status": "Pending", "total": 25})
|
|
735
|
+
orders.create({"user_id": jane.id, "status": "Pending", "total": 30})
|
|
736
|
+
orders.create({"user_id": jane.id, "status": "In Progress", "total": 35})
|
|
737
|
+
|
|
738
|
+
# Another Jane's orders
|
|
739
|
+
orders.create({"user_id": another_jane.id, "status": "Pending", "total": 15})
|
|
740
|
+
|
|
741
|
+
# Bob's orders
|
|
742
|
+
orders.create({"user_id": bob.id, "status": "Pending", "total": 28})
|
|
743
|
+
orders.create({"user_id": bob.id, "status": "In Progress", "total": 35})
|
|
744
|
+
|
|
745
|
+
# return all orders for anyone named Jane that have a status of Pending
|
|
746
|
+
return orders.join("join users on users.id=orders.user_id").where("users.name=Jane").sort_by("total", "asc").where("status=Pending")
|
|
747
|
+
|
|
748
|
+
cli = clearskies.contexts.Cli(
|
|
749
|
+
clearskies.endpoints.Callable(
|
|
750
|
+
my_application,
|
|
751
|
+
model_class=Order,
|
|
752
|
+
readable_column_names=["user", "total"],
|
|
753
|
+
),
|
|
754
|
+
classes=[Order, User],
|
|
755
|
+
)
|
|
756
|
+
cli()
|
|
757
|
+
|
|
758
|
+
```
|
|
759
|
+
"""
|
|
481
760
|
self.no_single_model()
|
|
482
761
|
return self.with_query(self.get_query().add_join(Join(join)))
|
|
483
762
|
|
|
@@ -498,6 +777,7 @@ class Model(Schema, InjectableProperties):
|
|
|
498
777
|
return False
|
|
499
778
|
|
|
500
779
|
def group_by(self: Self, group_by_column_name: str) -> Self:
|
|
780
|
+
"""Add a group by clause to the query."""
|
|
501
781
|
self.no_single_model()
|
|
502
782
|
return self.with_query(self.get_query().set_group_by(group_by_column_name))
|
|
503
783
|
|
|
@@ -510,6 +790,41 @@ class Model(Schema, InjectableProperties):
|
|
|
510
790
|
secondary_direction: str = "",
|
|
511
791
|
secondary_table_name: str = "",
|
|
512
792
|
) -> Self:
|
|
793
|
+
"""
|
|
794
|
+
Add a sort by clause to the query. You can sort by up to two columns at once.
|
|
795
|
+
|
|
796
|
+
Example:
|
|
797
|
+
```
|
|
798
|
+
import clearskies
|
|
799
|
+
|
|
800
|
+
class Order(clearskies.Model):
|
|
801
|
+
id_column_name = "id"
|
|
802
|
+
backend = clearskies.backends.MemoryBackend()
|
|
803
|
+
|
|
804
|
+
id = clearskies.columns.Uuid()
|
|
805
|
+
user_id = clearskies.columns.String()
|
|
806
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
807
|
+
total = clearskies.columns.Float()
|
|
808
|
+
|
|
809
|
+
def my_application(orders):
|
|
810
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
811
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
812
|
+
orders.create({"user_id": "Alice", "status": "Pending", "total": 30})
|
|
813
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 26})
|
|
814
|
+
|
|
815
|
+
return orders.sort_by("user_id", "asc", secondary_column_name="total", secondary_direction="desc")
|
|
816
|
+
|
|
817
|
+
cli = clearskies.contexts.Cli(
|
|
818
|
+
clearskies.endpoints.Callable(
|
|
819
|
+
my_application,
|
|
820
|
+
model_class=Order,
|
|
821
|
+
readable_column_names=["user_id", "total"],
|
|
822
|
+
),
|
|
823
|
+
classes=[Order],
|
|
824
|
+
)
|
|
825
|
+
cli()
|
|
826
|
+
```
|
|
827
|
+
"""
|
|
513
828
|
self.no_single_model()
|
|
514
829
|
sort = Sort(primary_table_name, primary_column_name, primary_direction)
|
|
515
830
|
secondary_sort = None
|
|
@@ -518,10 +833,96 @@ class Model(Schema, InjectableProperties):
|
|
|
518
833
|
return self.with_query(self.get_query().set_sort(sort, secondary_sort))
|
|
519
834
|
|
|
520
835
|
def limit(self: Self, limit: int) -> Self:
|
|
836
|
+
"""
|
|
837
|
+
Set the number of records to return.
|
|
838
|
+
|
|
839
|
+
```
|
|
840
|
+
import clearskies
|
|
841
|
+
|
|
842
|
+
class Order(clearskies.Model):
|
|
843
|
+
id_column_name = "id"
|
|
844
|
+
backend = clearskies.backends.MemoryBackend()
|
|
845
|
+
|
|
846
|
+
id = clearskies.columns.Uuid()
|
|
847
|
+
user_id = clearskies.columns.String()
|
|
848
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
849
|
+
total = clearskies.columns.Float()
|
|
850
|
+
|
|
851
|
+
def my_application(orders):
|
|
852
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
853
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
854
|
+
orders.create({"user_id": "Alice", "status": "Pending", "total": 30})
|
|
855
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 26})
|
|
856
|
+
|
|
857
|
+
return orders.limit(2)
|
|
858
|
+
|
|
859
|
+
cli = clearskies.contexts.Cli(
|
|
860
|
+
clearskies.endpoints.Callable(
|
|
861
|
+
my_application,
|
|
862
|
+
model_class=Order,
|
|
863
|
+
readable_column_names=["user_id", "total"],
|
|
864
|
+
),
|
|
865
|
+
classes=[Order],
|
|
866
|
+
)
|
|
867
|
+
cli()
|
|
868
|
+
```
|
|
869
|
+
"""
|
|
521
870
|
self.no_single_model()
|
|
522
871
|
return self.with_query(self.get_query().set_limit(limit))
|
|
523
872
|
|
|
524
873
|
def pagination(self: Self, **pagination_data) -> Self:
|
|
874
|
+
"""
|
|
875
|
+
Set the pagination parameter(s) for the query.
|
|
876
|
+
|
|
877
|
+
The exact details of how pagination work depend on the backend. For instance, the cursor and memory backend
|
|
878
|
+
expect to be given a `start` parameter, while an API backend will vary with the API, and the dynamodb backend
|
|
879
|
+
expects a kwarg called `cursor`. As a result, it's necessary to check the backend documentation to understand
|
|
880
|
+
how to properly set pagination. The endpoints automatically account for this because backends are required
|
|
881
|
+
to declare pagination details via the `allowed_pagination_keys` method. If you attempt to set invalid
|
|
882
|
+
pagination data via this method, clearskies will raise a ValueError.
|
|
883
|
+
|
|
884
|
+
Example:
|
|
885
|
+
```
|
|
886
|
+
import clearskies
|
|
887
|
+
|
|
888
|
+
class Order(clearskies.Model):
|
|
889
|
+
id_column_name = "id"
|
|
890
|
+
backend = clearskies.backends.MemoryBackend()
|
|
891
|
+
|
|
892
|
+
id = clearskies.columns.Uuid()
|
|
893
|
+
user_id = clearskies.columns.String()
|
|
894
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
895
|
+
total = clearskies.columns.Float()
|
|
896
|
+
|
|
897
|
+
def my_application(orders):
|
|
898
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
899
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
900
|
+
orders.create({"user_id": "Alice", "status": "Pending", "total": 30})
|
|
901
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 26})
|
|
902
|
+
|
|
903
|
+
return orders.sort_by("total", "asc").pagination(start=2)
|
|
904
|
+
|
|
905
|
+
cli = clearskies.contexts.Cli(
|
|
906
|
+
clearskies.endpoints.Callable(
|
|
907
|
+
my_application,
|
|
908
|
+
model_class=Order,
|
|
909
|
+
readable_column_names=["user_id", "total"],
|
|
910
|
+
),
|
|
911
|
+
classes=[Order],
|
|
912
|
+
)
|
|
913
|
+
cli()
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
However, if the return line in `my_application` is switched for either of these:
|
|
917
|
+
|
|
918
|
+
```
|
|
919
|
+
return orders.sort_by("total", "asc").pagination(start="asdf")
|
|
920
|
+
return orders.sort_by("total", "asc").pagination(something_else=5)
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
Will result in an exception that explains exactly what is wrong.
|
|
924
|
+
|
|
925
|
+
"""
|
|
525
926
|
self.no_single_model()
|
|
526
927
|
error = self.backend.validate_pagination_data(pagination_data, str)
|
|
527
928
|
if error:
|
|
@@ -538,8 +939,36 @@ class Model(Schema, InjectableProperties):
|
|
|
538
939
|
This is just shorthand for `models.where("column=value").find()`. Example:
|
|
539
940
|
|
|
540
941
|
```python
|
|
541
|
-
|
|
542
|
-
|
|
942
|
+
import clearskies
|
|
943
|
+
|
|
944
|
+
class Order(clearskies.Model):
|
|
945
|
+
id_column_name = "id"
|
|
946
|
+
backend = clearskies.backends.MemoryBackend()
|
|
947
|
+
|
|
948
|
+
id = clearskies.columns.Uuid()
|
|
949
|
+
user_id = clearskies.columns.String()
|
|
950
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
951
|
+
total = clearskies.columns.Float()
|
|
952
|
+
|
|
953
|
+
def my_application(orders):
|
|
954
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
955
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
956
|
+
orders.create({"user_id": "Jane", "status": "Pending", "total": 30})
|
|
957
|
+
|
|
958
|
+
jane = orders.find("user_id=Jane")
|
|
959
|
+
jane.total = 35
|
|
960
|
+
jane.save()
|
|
961
|
+
|
|
962
|
+
return {
|
|
963
|
+
"user_id": jane.user_id,
|
|
964
|
+
"total": jane.total,
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
cli = clearskies.contexts.Cli(
|
|
968
|
+
my_application,
|
|
969
|
+
classes=[Order],
|
|
970
|
+
)
|
|
971
|
+
cli()
|
|
543
972
|
```
|
|
544
973
|
"""
|
|
545
974
|
self.no_single_model()
|
|
@@ -564,13 +993,48 @@ class Model(Schema, InjectableProperties):
|
|
|
564
993
|
"""
|
|
565
994
|
Loop through all available pages of results and returns a list of all models that match the query.
|
|
566
995
|
|
|
567
|
-
|
|
568
|
-
|
|
996
|
+
If you don't set a limit on a query, some backends will return all records but some backends have a
|
|
997
|
+
default maximum number of results that they will return. In the latter case, you can use `paginate_all`
|
|
998
|
+
to fetch all records by instructing clearskies to iterate over all pages. This is possible because backends
|
|
999
|
+
are required to define how pagination works in a way that clearskies can automatically understand and
|
|
1000
|
+
use. To demonstrate this, the following example sets a limit of 1 which stops the memory backend
|
|
1001
|
+
from returning everything, and then uses `paginate_all` to fetch all records. The memory backend
|
|
1002
|
+
doesn't have a default limit, so in practice the `paginate_all` is unnecessary here, but this is done
|
|
1003
|
+
for demonstration purposes.
|
|
569
1004
|
|
|
570
|
-
```python
|
|
571
|
-
for model in models.where("column=value").paginate_all():
|
|
572
|
-
print(model.id)
|
|
573
1005
|
```
|
|
1006
|
+
import clearskies
|
|
1007
|
+
|
|
1008
|
+
class Order(clearskies.Model):
|
|
1009
|
+
id_column_name = "id"
|
|
1010
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1011
|
+
|
|
1012
|
+
id = clearskies.columns.Uuid()
|
|
1013
|
+
user_id = clearskies.columns.String()
|
|
1014
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1015
|
+
total = clearskies.columns.Float()
|
|
1016
|
+
|
|
1017
|
+
def my_application(orders):
|
|
1018
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
1019
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
1020
|
+
orders.create({"user_id": "Alice", "status": "Pending", "total": 30})
|
|
1021
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 26})
|
|
1022
|
+
|
|
1023
|
+
return orders.limit(1).paginate_all()
|
|
1024
|
+
|
|
1025
|
+
cli = clearskies.contexts.Cli(
|
|
1026
|
+
clearskies.endpoints.Callable(
|
|
1027
|
+
my_application,
|
|
1028
|
+
model_class=Order,
|
|
1029
|
+
readable_column_names=["user_id", "total"],
|
|
1030
|
+
),
|
|
1031
|
+
classes=[Order],
|
|
1032
|
+
)
|
|
1033
|
+
cli()
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
NOTE: this loads up all records in memory before returning (e.g. it isn't using generators yet), so
|
|
1037
|
+
expect delays for large record sets.
|
|
574
1038
|
"""
|
|
575
1039
|
self.no_single_model()
|
|
576
1040
|
next_models = self.with_query(self.get_query())
|
|
@@ -611,11 +1075,43 @@ class Model(Schema, InjectableProperties):
|
|
|
611
1075
|
|
|
612
1076
|
def first(self: Self) -> Self:
|
|
613
1077
|
"""
|
|
614
|
-
Return the first model
|
|
1078
|
+
Return the first model for a given query.
|
|
1079
|
+
|
|
1080
|
+
The `where` method returns an object meant to be iterated over. If you are expecting your query to return a single
|
|
1081
|
+
record, then you can use first to turn that directly into the matching model so you don't have to iterate over it:
|
|
1082
|
+
|
|
1083
|
+
```
|
|
1084
|
+
import clearskies
|
|
1085
|
+
|
|
1086
|
+
class Order(clearskies.Model):
|
|
1087
|
+
id_column_name = "id"
|
|
1088
|
+
backend = clearskies.backends.MemoryBackend()
|
|
1089
|
+
|
|
1090
|
+
id = clearskies.columns.Uuid()
|
|
1091
|
+
user_id = clearskies.columns.String()
|
|
1092
|
+
status = clearskies.columns.Select(["Pending", "In Progress"])
|
|
1093
|
+
total = clearskies.columns.Float()
|
|
1094
|
+
|
|
1095
|
+
def my_application(orders):
|
|
1096
|
+
orders.create({"user_id": "Bob", "status": "Pending", "total": 25})
|
|
1097
|
+
orders.create({"user_id": "Alice", "status": "In Progress", "total": 15})
|
|
1098
|
+
orders.create({"user_id": "Jane", "status": "Pending", "total": 30})
|
|
1099
|
+
|
|
1100
|
+
jane = orders.where("status=Pending").where(Order.total.greater_than(25)).first()
|
|
1101
|
+
jane.total = 35
|
|
1102
|
+
jane.save()
|
|
1103
|
+
|
|
1104
|
+
return {
|
|
1105
|
+
"user_id": jane.user_id,
|
|
1106
|
+
"total": jane.total,
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
cli = clearskies.contexts.Cli(
|
|
1110
|
+
my_application,
|
|
1111
|
+
classes=[Order],
|
|
1112
|
+
)
|
|
1113
|
+
cli()
|
|
615
1114
|
|
|
616
|
-
```python
|
|
617
|
-
model = models.where("column=value").sort_by("age", "DESC").first()
|
|
618
|
-
print(model.id)
|
|
619
1115
|
```
|
|
620
1116
|
"""
|
|
621
1117
|
self.no_single_model()
|
clearskies/query/query.py
CHANGED
|
@@ -99,13 +99,13 @@ class Query:
|
|
|
99
99
|
"""Return the properties of this query as a dictionary so it can be used as kwargs when creating another one."""
|
|
100
100
|
return {
|
|
101
101
|
"model_class": self.model_class,
|
|
102
|
-
"conditions": self.conditions,
|
|
103
|
-
"joins": self.joins,
|
|
104
|
-
"sorts": self.sorts,
|
|
102
|
+
"conditions": [*self.conditions],
|
|
103
|
+
"joins": [*self.joins],
|
|
104
|
+
"sorts": [*self.sorts],
|
|
105
105
|
"limit": self.limit,
|
|
106
106
|
"group_by": self.group_by,
|
|
107
107
|
"pagination": self.pagination,
|
|
108
|
-
"selects": self.selects,
|
|
108
|
+
"selects": [*self.selects],
|
|
109
109
|
"select_all": self.select_all,
|
|
110
110
|
}
|
|
111
111
|
|
clearskies/security_header.py
CHANGED
|
@@ -2,6 +2,13 @@ from clearskies.configurable import Configurable
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class SecurityHeader(Configurable):
|
|
5
|
+
"""
|
|
6
|
+
Attach all the various security headers to endpoints.
|
|
7
|
+
|
|
8
|
+
The security header classes can be attached directly to both endpoints and endpoint groups and
|
|
9
|
+
are used to set all the various security headers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
5
12
|
is_cors = False
|
|
6
13
|
|
|
7
14
|
def set_headers_for_input_output(self, input_output):
|
clearskies/validator.py
CHANGED
|
@@ -11,6 +11,18 @@ if TYPE_CHECKING:
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Validator(ABC, configurable.Configurable):
|
|
14
|
+
"""
|
|
15
|
+
Attach input validation rules to columns!
|
|
16
|
+
|
|
17
|
+
The validators provide a way to attach input validation logic to columns. The columns themselves already
|
|
18
|
+
provide basic validation (making sure strings are strings, integers are integers, etc...) but these classes
|
|
19
|
+
allow for more detailed rules.
|
|
20
|
+
|
|
21
|
+
It's important to understand that validators only apply to client input, which means that input validation
|
|
22
|
+
is only enforced by appropriate endpoints. If you inject a model into a function of your own and execute
|
|
23
|
+
a save operation with it, validators will **NOT** be checked.
|
|
24
|
+
"""
|
|
25
|
+
|
|
14
26
|
is_unique = False
|
|
15
27
|
is_required = False
|
|
16
28
|
|
|
File without changes
|
|
File without changes
|