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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: clear-skies
3
- Version: 2.0.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/cmancone/clearskies
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=rMzX5pveW00RXrAujLGqxOhCMLnvOlAxdQTwl-kCEQk,1203
5
- clearskies/authentication/authorization.py,sha256=SMbEhX-kwvV5ivFToEVcGR7xqm95DkRg4RAPVLLStOc,534
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=LPDYjZVVK8aoDUERSyE5pjoac8nA9_TaQtiZRLIZLcs,4826
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=DP3M5rMW48CToD_ITlrYpaHGyOanyBVGMQQmLo6BK6Y,17608
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=1IgX4ts9x8qxoVS2csdDflimVQx0leeVNhvgeUaFn40,5050
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=PKpHsGihsMP4Ph1xzbCvvgjpt5NuiNpGZUGLKvXlNVM,52091
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=xoll5EalODwI_W1HtWutAIJ_ndzLyWLqvfZonVUtmYY,5017
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=YD7uDn1bbW4TYlcFjKFvx90HoSGbIYn77qjdAHm6z9E,833
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=1y7g45FiHbI3Q6JEmwskWVuXPWgmddyK49FfgSafR5U,227
141
- clearskies/contexts/context.py,sha256=ESnzmhZ7pjDVsmbT7eZTsR26ZvgV-aeeUGsDsT1oK0s,3109
142
- clearskies/contexts/wsgi.py,sha256=xXHb3m8m44dHKsPX4WLIFExWg5Do2Zv6mQtyb4V4pnM,547
143
- clearskies/contexts/wsgi_ref.py,sha256=awy-J4DELrVyD5E9FysPE5A_W9JLEZOf2g1hIAogJUA,1733
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=g6v2Pjga1EUJ6BaHlX8HL3qoK1z6l_iPl7oNN6eJD9Y,45131
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=4Wzl89zmNwhKDGqMMlQhzy5MJWx5x_hkbYMDdNguFzw,48817
166
- clearskies/endpoint_group.py,sha256=ZAkkUsOyd5GFyh_kWFezuO7f8dOzS49pH9bcaaDuW7A,10997
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=fdVAa2Dhfg3-OikoE9-PiDnJuZRD0MarQKcdRGwlnaA,13904
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=qdGco45F2icRLxhNy5Chtuww-q72jac6eoUpvFn40bU,26492
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=J_fJ8XZXvZxZ0nZd276WXaxzOFb-bVZ-PTaAqddGhOk,6076
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=__gPaE6F_MQKpt1cFh_ZOky7I44K3yiy0Z9FeEYscK0,202
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=u9xNOfXbDOTpP1XLu-5O_QGJ6Ja674HRCW6QCxtlD1c,723
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.0.dist-info/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
246
- clear_skies-2.0.0.dist-info/METADATA,sha256=JLZ7NxtWMmVIm5nNmlP1XYzqAagPw-g11XmJpJ_Ndgo,1757
247
- clear_skies-2.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
248
- clear_skies-2.0.0.dist-info/RECORD,,
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,,
@@ -12,6 +12,10 @@ if TYPE_CHECKING:
12
12
 
13
13
 
14
14
  class Authentication(clearskies.configurable.Configurable, requests.auth.AuthBase):
15
+ """
16
+ Authentication!
17
+ """
18
+
15
19
  is_public = True
16
20
  can_authorize = False
17
21
  has_dynamic_credentials = False
@@ -1,4 +1,8 @@
1
1
  class Authorization:
2
+ """
3
+ Authorization!
4
+ """
5
+
2
6
  def gate(self, authorization_data, input_output):
3
7
  """
4
8
  Return True/False to denote if the given user, as represented by the authorization data, should be allowed access.
@@ -10,8 +10,13 @@ from clearskies.security_headers.cors import Cors
10
10
 
11
11
 
12
12
  class Jwks(Authentication, clearskies.di.InjectableProperties):
13
- """The URL where the JWKS can be found."""
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.secret_key
498
+ if self.alternate_secret_key
499
499
  else self.environment.get(self.alternate_environment_key)
500
500
  )
501
501
  return self._alternate_secret
@@ -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
- The base column.
27
+ Columns are used to build schemes and enable a variety of levels of automation with clearskies.
28
28
 
29
- This class (well, the children that extend it) are used to define the columns that exist in a given model class -
30
- they help you build your schema. The column definitions are then used by handlers and other aspects of the
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
  """
@@ -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",
@@ -1,5 +1,8 @@
1
+ from typing import Any
2
+
3
+
1
4
  class Config:
2
- def __init__(self, required=False, default=None):
5
+ def __init__(self, required: bool = False, default: Any = None):
3
6
  self.required = required
4
7
  self.default = default
5
8
 
@@ -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)
@@ -3,5 +3,9 @@ from clearskies.input_outputs import Cli as CliInputOutput
3
3
 
4
4
 
5
5
  class Cli(Context):
6
+ """
7
+ Run an application via a CLI command
8
+ """
9
+
6
10
  def __call__(self): # type: ignore
7
11
  return self.execute_application(CliInputOutput())
@@ -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,
@@ -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))
@@ -12,6 +12,10 @@ from clearskies.input_outputs import Wsgi as WsgiInputOutput
12
12
 
13
13
 
14
14
  class WsgiRef(Context):
15
+ """
16
+ Use a built in WSGI server (for development purposes only).
17
+ """
18
+
15
19
  port: int = 8080
16
20
 
17
21
  def __init__(
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 there you don't often have
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
- Endpoints - the clearskies workhorse.
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. Each endpoint has their own configuration settings, but there are some configuration
43
- settings that are common to all endpoints, which are listed below:
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 majority of endpoints require a model class that tells the endpoint where to get/save its data.
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
 
@@ -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
@@ -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 endpoin is invoked. Like
27
- all callables invoked by clearskies, you can request any defined depenndency that can be provided by the clearskies
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 an model instance, a model query, or a list of model instances and the callable endpoint will automatically 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 three things:
22
+ To be useable, a model class needs four things:
23
23
 
24
- 1. Column definitions
25
- 2. The name of the id column
26
- 3. A backend
27
- 4. A destination name (equivalent to a table name for SQL backends)
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 the given condition to the query.
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
- This method returns a new object with the updated query. The original model object is unmodified.
658
+ class Order(clearskies.Model):
659
+ id_column_name = "id"
660
+ backend = clearskies.backends.MemoryBackend()
460
661
 
461
- Conditions should be an SQL-like string of the form [column][operator][value] with an optional table prefix.
462
- You can safely inject user input into the value. The column name will also be checked against the searchable
463
- columns for the model class, and an exception will be thrown if the column doesn't exist or is not searchable.
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
- Multiple conditions are always joined with AND. There is no explicit option for OR. The closest is using an
466
- IN condition.
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
- Examples:
469
- ```python
470
- for record in (
471
- models.where("order_id=5").where("status IN ('ACTIVE','PENDING')").where("other_table.id=asdf")
472
- ):
473
- print(record.id)
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
- """Add a join clause to the query."""
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
- model = models.find("column=value")
542
- print(model.id)
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
- NOTE: this loads up all records in memory before returning (e.g. it isn't using generators yet), so
568
- expect delays for large record sets.
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 matching the given query.
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
 
@@ -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