clear-skies 2.0.9__py3-none-any.whl → 2.0.11__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.4
2
2
  Name: clear-skies
3
- Version: 2.0.9
3
+ Version: 2.0.11
4
4
  Summary: A framework for building backends in the cloud
5
5
  Project-URL: Documentation, https://clearskies.io/
6
6
  Project-URL: Repository, https://github.com/clearskies-py/clearskies
@@ -4,8 +4,8 @@ clearskies/column.py,sha256=3zAc5pc8zo4ws0mEU5GfbuvljkpAJJlAxmAf8lBKAVA,51299
4
4
  clearskies/configurable.py,sha256=gfX9o5qgMqc8lSzrdcUyR3b6zdOvg9ccYBQ0VvZjXkk,3085
5
5
  clearskies/decorators.py,sha256=lcXJC4xI-hRyKDSH8yklcWjOrIL9k2Qks1xP2lmyDm0,1024
6
6
  clearskies/decorators.pyi,sha256=qSRzPJYYoV9trEsh_V1BxP2HdMz0G2gOs4FV8f5hvyU,388
7
- clearskies/end.py,sha256=5XdtQ2Zfe7Ifqf8AvMvswtyYzxKgTTSuENxUOgym5HM,10688
8
- clearskies/endpoint.py,sha256=cAbba47XbeRl_XKOsl0p2yTECOQ19rAIVryIoEHt3_M,47972
7
+ clearskies/end.py,sha256=mEPdBB4lh9zmAKTbsffCh-mQMZGIVohriWjm-952EzA,10629
8
+ clearskies/endpoint.py,sha256=z4ys46jZC1rFSAfwdc-eScUDThcJ4mILRCQMcqvAX68,47960
9
9
  clearskies/endpoint_group.py,sha256=qsxQpt1i6IRvQqhQkt_udoVj0TMdoIOccvKyEBaeaEg,12343
10
10
  clearskies/environment.py,sha256=BbNgQH3uuCTxoHeLB9O1_HsPTaJL5aiXqY6hZ9aj-jo,3739
11
11
  clearskies/model.py,sha256=1cCx1KcAOK6_MHBdgtWdu4lZMYuPQaWHSrXsdW4v7V8,77545
@@ -68,7 +68,7 @@ clearskies/backends/cursor_backend.py,sha256=5YBO0EwxU_1YKIL3_fIbA93sh7NG4Qg8jpA
68
68
  clearskies/backends/memory_backend.py,sha256=H_2fib9EFdXsg3ZMMpC20DMHnrnYDbPgZnmiarbOhiM,33963
69
69
  clearskies/backends/secrets_backend.py,sha256=6YVx154vhFLt_Fz0dz8wjQiVbaqBzPoUlfsYXU-9h7s,4044
70
70
  clearskies/columns/__init__.py,sha256=a__-1hv-aMV3RU0Sd-BEkfItSJaG51EF2VeRQoTdka8,1990
71
- clearskies/columns/audit.py,sha256=LRMUtmZE7ca-k_6A57J2bDUJzU1o6hU6aFwU7sXe8c0,7594
71
+ clearskies/columns/audit.py,sha256=KABjKk3YdV6G_43OdkhFA_iGkmjP9Sb-m0Beh2_aXU4,7624
72
72
  clearskies/columns/belongs_to_id.py,sha256=Ga3IpWlMy2_m8e5OK_iortNjHdMbF1RQF2tglWAzBVc,17575
73
73
  clearskies/columns/belongs_to_model.py,sha256=nZ-yOs5CJx_WoMQ2wLLQfwSTfuF5vantEL0DFUgwC4Q,5714
74
74
  clearskies/columns/belongs_to_self.py,sha256=cmgjxq4SmokGO2UbE0VlcFRXack9KjPLzyE9L3fNang,3725
@@ -103,7 +103,7 @@ clearskies/columns/timestamp.py,sha256=nIEbvg1YqKPjtEeFyYRnPPPSYB_lYIIUUm8zNoE8d
103
103
  clearskies/columns/updated.py,sha256=BFan3JnO2IEbryTGC2rR_RD1ruDcev49owPBYyjjSVI,3340
104
104
  clearskies/columns/uuid.py,sha256=QE1UKHA9zt7bYd59rYRmzIHSonq9-sZeQbAIQtoJC54,2265
105
105
  clearskies/configs/README.md,sha256=4-r2FvkrBTrKkcKgJ5O3Qj1RZ7nnkknenKze4XYlQDs,4827
106
- clearskies/configs/__init__.py,sha256=X3U5l_oXYh9JTeiI7IZuzz1rq5TTO4HHIvcz5gLGa9s,5357
106
+ clearskies/configs/__init__.py,sha256=dCuGWN6eI7r9hW8d33R4nCehEMdgEpvJU1TQ-NWVK6M,5401
107
107
  clearskies/configs/actions.py,sha256=5OwrokU8uTSSQ_6SvL94SOP5JVkvRRFGAXNcCdMt8aQ,1388
108
108
  clearskies/configs/any.py,sha256=QSLOO955Lbg8NqYeyxdQGXTTyfFc5P036LORUa7avHc,388
109
109
  clearskies/configs/any_dict.py,sha256=LECTIcCF22JXbAaZdWSmKh9UShRuRt8PydNCuj05ijI,907
@@ -123,9 +123,10 @@ clearskies/configs/email_list.py,sha256=cTgdQTz4HYkqbdTEHPy8LcLjPGqFWY5TOIH8cHbf
123
123
  clearskies/configs/email_list_or_callable.py,sha256=jowDly4ZKLOYJ73aJ8VoNX338jzqTZyIcY91ANvrgt0,679
124
124
  clearskies/configs/email_or_email_list_or_callable.py,sha256=tFdAsI1sTDMOheb3h8nQhIVxnl9xzZPwZtKjYEHjkKM,2470
125
125
  clearskies/configs/endpoint.py,sha256=NEDthtdPzejAot7yzUjC01PvQJR9BIyk5YnWXCi1w9I,759
126
- clearskies/configs/endpoint_list.py,sha256=9xtXmZpdOh7uk9Sm3MF9xi1_N-n2DZ-F4s0zrNfEKng,1200
126
+ clearskies/configs/endpoint_list.py,sha256=HS1IO2HX8Y0W5ZZU7_Yk_6p5a8mo93an5Q2KAkOl9ZU,1202
127
127
  clearskies/configs/float.py,sha256=7HwDMf-3VfrZxTmXwpnwDgXyzujUBpz1xPi1StdjB1Q,621
128
128
  clearskies/configs/float_or_callable.py,sha256=9IiylL4Gg9_7HJOrYxbUPuLGE03TwKsAMsxfB7Lh2yY,744
129
+ clearskies/configs/headers.py,sha256=D3bo3jE8x5DA1vLeo9ewnfIBHcRbr0JavOY-SQyFeVQ,986
129
130
  clearskies/configs/integer.py,sha256=lw05RhD8ZkHO_tYJLtCUHZ_Ucz2Mf7C0Z5wVOsMqgq0,620
130
131
  clearskies/configs/integer_or_callable.py,sha256=acCUP2ZSfSMPyID2LzIOQ-dlgARpxEktgr7KFWMtGbk,739
131
132
  clearskies/configs/joins.py,sha256=i1QmxFwkpL-pnzVPh3mrHpwrvzsjt9nH5pMSj4J1TT4,944
@@ -155,14 +156,14 @@ clearskies/configs/validators.py,sha256=aAvf5dgncR8OP7_M7QUNhsHau4wAdPiXw9wNARRA
155
156
  clearskies/configs/writeable_model_column.py,sha256=22pMoOAwYjvhHBcFrBb6ImtuoahbDKd5jZrNsbykrw4,355
156
157
  clearskies/configs/writeable_model_columns.py,sha256=vQFh5w6ToC8SDi_GaEvy33csp2CTFIroAfOLigHjFKo,359
157
158
  clearskies/contexts/__init__.py,sha256=f7XVUq2UKlDH6fjmcUWk6lbe9p_OaGpZ5ZjM6CuwTGQ,247
158
- clearskies/contexts/cli.py,sha256=5t5rJJxlpNIrn8ITFIDvbSDbcPruoMQgei4ZjJ9Txvg,2648
159
- clearskies/contexts/context.py,sha256=CCaO8bXR2ZsB0HiFIQ5tY65N7Y5d1TxYHIj2d1S8sv8,3559
160
- clearskies/contexts/wsgi.py,sha256=scuV3FbDT4u51o4ND6XR60d196vRb3wdvcGiZ0e0SFg,2933
159
+ clearskies/contexts/cli.py,sha256=cuGWoyRhHlO_Ba6Dozg3sGob1VZoI4TBFOLu-2Udabk,2838
160
+ clearskies/contexts/context.py,sha256=rT-d0v5wiVqRNz_bsitTBXYEy0555-H2xr9TpETf-og,3676
161
+ clearskies/contexts/wsgi.py,sha256=7Vhh-gX2RPZUXad1hq9IkYx1JASyfV5xoPlwMOwjHso,3188
161
162
  clearskies/contexts/wsgi_ref.py,sha256=Z4oBIYeSsLp93dR1eBsZTaevzVYB0QrR-ugp1CQVltU,2822
162
163
  clearskies/di/__init__.py,sha256=Ab8GNv9ZksnCABq8n2gCcyLEAXD-5-kX4O8PweTJIFs,474
163
164
  clearskies/di/additional_config.py,sha256=65INxw8aqTZQsyaKPj-aQmd6FBe4_4DwibXGgWYBy14,5139
164
165
  clearskies/di/additional_config_auto_import.py,sha256=XYw0Kcnp6hp-ee-c0YjiATwJvRb2E82xk9PuoX9dGRY,758
165
- clearskies/di/di.py,sha256=XHlsyzWyU1adlvQlRYLcygDT3XGJptP4_qtHqSE4o-I,45461
166
+ clearskies/di/di.py,sha256=mW7Tk6-9pI5khq9h_A2kXJ3XDytwC6JdLX3TcLxU2q0,46348
166
167
  clearskies/di/injectable.py,sha256=TTgqhx494470I61-88BUQmHmevfat-wXVseKl8pQOEk,852
167
168
  clearskies/di/injectable_properties.py,sha256=yJP0J7l7tjG2soyXtrfDgktE7M8tQHaP-55Cmtq0b7M,6466
168
169
  clearskies/di/inject/__init__.py,sha256=plEkWId-VyhvqX5KM2HhdCqi7_ZJzPmFz69cPAo812Y,643
@@ -205,12 +206,12 @@ clearskies/functional/routing.py,sha256=tfIvP_Y29GTGr91_1ec3LSQFoTRwpkqU4BYHXPnB
205
206
  clearskies/functional/string.py,sha256=ZnkOjx8nxqZq2TV0CIb-Kz4onGoyekTX_WkLJM6XTmM,3311
206
207
  clearskies/functional/validations.py,sha256=cPYOTwWomlQrPvqPP_Jdlds7zZ5H9GABCP5pnGzC9T4,2821
207
208
  clearskies/input_outputs/__init__.py,sha256=9qeKJULw3MQ3zqkgBZice5d7qqRgsP3y-wkhWO2Y9vM,362
208
- clearskies/input_outputs/cli.py,sha256=xWv6gcDKL2I7_WmmQn91IQwyR9oS-WJAdjhsc0Yvux8,6061
209
- clearskies/input_outputs/headers.py,sha256=R0RVVAVRVwDUiMpMrh684eYNv-1Xr8I0NzWRVIPv2Kk,1652
210
- clearskies/input_outputs/input_output.py,sha256=Lg2Uv5aaSGIV14Uh7hRDfj3Wctatgye0lg8TMsHrNJs,5945
211
- clearskies/input_outputs/programmatic.py,sha256=l00VE_yPl5vMHTc8wDvybn65c3ceCTeWNz6hjL8vev4,1717
209
+ clearskies/input_outputs/cli.py,sha256=t7uWqLi6VI3i_zuyoKLdIq3vUwr19lQZoJmuAxVEvgg,5741
210
+ clearskies/input_outputs/headers.py,sha256=qdnPEUJ2amKUdzasbTQb015KGzJP-d4RjIbtrPdqTqM,1680
211
+ clearskies/input_outputs/input_output.py,sha256=tJQVN3U3MX_jpwsXJ-g-K1cdqQwyuSarTjo3JOp7zQQ,5154
212
+ clearskies/input_outputs/programmatic.py,sha256=OCRq0M42cKZKgk4YAfJyTWo3T4jNRmnGmVr7zCTovpg,1658
212
213
  clearskies/input_outputs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
213
- clearskies/input_outputs/wsgi.py,sha256=XU3g2IXm5Q_OLlvp3Iw1ORt0vQX_Ik9u_rLDb6kRfmk,2546
214
+ clearskies/input_outputs/wsgi.py,sha256=-utkoQnA4SCAJ-9Ydr-zTG8W5Hbsm4n91-i6Yu6GM7U,2749
214
215
  clearskies/input_outputs/exceptions/__init__.py,sha256=3KiM3KaMYEKoToqCCQ4_no2n0W5ROqeBC0sI2Ix4P6w,82
215
216
  clearskies/input_outputs/exceptions/cli_input_error.py,sha256=kOFU8aLTLmeTL_AKDshxMu8_ufildg6p8ndhE1xHfb0,41
216
217
  clearskies/input_outputs/exceptions/cli_not_found.py,sha256=JBBuZA9ZwdkPhd3a0qaGgEPQrxh1fehy4R3ZaV2gWXU,39
@@ -221,7 +222,7 @@ clearskies/query/query.py,sha256=0XR3fNhOpDNJY0US2oseAS3p3Y0jxxVs86P6vWEvUcA,606
221
222
  clearskies/query/sort.py,sha256=c-EtIkjg3kLjwSTdXD7sfyx-mNUhAepUV-2izprh3iY,754
222
223
  clearskies/secrets/__init__.py,sha256=G-A8YhCMlS_OdboSeKzCZp6iwfqwU4BPEnB5HvD88wY,142
223
224
  clearskies/secrets/akeyless.py,sha256=4SwnVNzMAijZtzR0Q25dizEw-q7nbS4G5s0CoGyc-G0,7219
224
- clearskies/secrets/secrets.py,sha256=20Oq-PogMbFCPB6b4HyowlDD9QNY4r4BVKVm9hTrGxs,1724
225
+ clearskies/secrets/secrets.py,sha256=9sYrI0PmxXAzyDCfylOmb8svXqPc9IefaWKaBPHrjxE,1815
225
226
  clearskies/secrets/additional_configs/__init__.py,sha256=cFCrbtKF5nuR061S2y1iKZp349x-y8Srdwe3VZbfSFU,1119
226
227
  clearskies/secrets/additional_configs/mysql_connection_dynamic_producer.py,sha256=CnIiXLVQdUnUey3dbCTXuNNP7Mmw1gjjNjZiBtfgGto,2757
227
228
  clearskies/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py,sha256=N8ruxrTNhvYlp3cYXq6V78KPPr4n40LM7QoXHvD8IZg,6235
@@ -250,7 +251,7 @@ clearskies/validators/minimum_value.py,sha256=NDLcG6xCemlv3kfr-RiUaM3x2INS1GJGMB
250
251
  clearskies/validators/required.py,sha256=GWxyexwj-K6DunZWNEnZxW6tQGAFd4oOCvQrW1s1K9k,1308
251
252
  clearskies/validators/timedelta.py,sha256=DJ0pTm-SSUtjZ7phGoD6vjb086vXPzvLLijkU-jQlOs,1892
252
253
  clearskies/validators/unique.py,sha256=GFEQOMYRIO9pSGHHj6zf1GdnJ0UM7Dm4ZO4uGn19BZo,991
253
- clear_skies-2.0.9.dist-info/METADATA,sha256=6MAexsDWPfHN-2JRQC4PE8iIm10D3skQejnBCESu1Y0,2113
254
- clear_skies-2.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
255
- clear_skies-2.0.9.dist-info/licenses/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
256
- clear_skies-2.0.9.dist-info/RECORD,,
254
+ clear_skies-2.0.11.dist-info/METADATA,sha256=KsFDLUKDhP1zHg3H2qmyDILiNZ4Ko0LfYTVyXUNywVM,2114
255
+ clear_skies-2.0.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
256
+ clear_skies-2.0.11.dist-info/licenses/LICENSE,sha256=3Ehd0g3YOpCj8sqj0Xjq5qbOtjjgk9qzhhD9YjRQgOA,1053
257
+ clear_skies-2.0.11.dist-info/RECORD,,
@@ -155,7 +155,7 @@ class Audit(HasMany):
155
155
  },
156
156
  )
157
157
 
158
- def post_delete(self, model):
158
+ def post_delete(self, model: Model) -> None:
159
159
  super().post_delete(model)
160
160
  exclude_columns = self.exclude_columns
161
161
  model_columns = self.get_model_columns()
@@ -167,7 +167,7 @@ class Audit(HasMany):
167
167
  continue
168
168
  final_data = {
169
169
  **final_data,
170
- **(model_columns[key].to_json(model) if key in model_columns else {key: model.data.get(key)}),
170
+ **(model_columns[key].to_json(model) if key in model_columns else {key: model.get_raw_data().get(key)}),
171
171
  }
172
172
 
173
173
  for key in mask_columns:
@@ -178,7 +178,7 @@ class Audit(HasMany):
178
178
  self.child_model.create(
179
179
  {
180
180
  "class": self.model_class.__name__,
181
- "resource_id": model.get(self.model_class.id_column_name),
181
+ "resource_id": getattr(model, self.model_class.id_column_name),
182
182
  "action": "delete",
183
183
  "data": final_data,
184
184
  }
@@ -87,6 +87,7 @@ from .endpoint import Endpoint
87
87
  from .endpoint_list import EndpointList
88
88
  from .float import Float
89
89
  from .float_or_callable import FloatOrCallable
90
+ from .headers import Headers
90
91
  from .integer import Integer
91
92
  from .integer_or_callable import IntegerOrCallable
92
93
  from .joins import Joins
@@ -139,6 +140,7 @@ __all__ = [
139
140
  "EndpointList",
140
141
  "Float",
141
142
  "FloatOrCallable",
143
+ "Headers",
142
144
  "Joins",
143
145
  "Integer",
144
146
  "IntegerOrCallable",
@@ -19,7 +19,7 @@ class EndpointList(config.Config):
19
19
  if not hasattr(item, "top_level_authentication_and_authorization"):
20
20
  error_prefix = self._error_prefix(instance)
21
21
  raise TypeError(
22
- 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
+ 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."
23
23
  )
24
24
  instance._set_config(self, value)
25
25
 
@@ -0,0 +1,30 @@
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.input_outputs import headers
9
+
10
+
11
+ class Headers(config.Config):
12
+ """
13
+ This is for a configuration that should be an instance of type clearskies.input_outputs.Headers.
14
+ """
15
+
16
+ def __set__(self, instance, value: headers.Headers):
17
+ if value is None:
18
+ return
19
+
20
+ if not hasattr(value, "_duck_cheat") or value._duck_cheat != "headers":
21
+ error_prefix = self._error_prefix(instance)
22
+ raise TypeError(
23
+ f"{error_prefix} attempt to set a value of type '{value.__class__.__name__}' to a property that expets an instance of clearskies.input_outputs.Headers"
24
+ )
25
+ instance._set_config(self, value)
26
+
27
+ def __get__(self, instance, parent) -> headers.Headers:
28
+ if not instance:
29
+ return self # type: ignore
30
+ return instance._get_config(self)
@@ -119,6 +119,11 @@ class Cli(Context):
119
119
  Although note that the first two are going to be preferred over the third, simply because with the
120
120
  third there's simply no way to specify the type of a variable. As a result, you may run into issues
121
121
  with strict type checking on endpoints.
122
+
123
+ ### Context Callables
124
+
125
+ When using the Cli context, an additional named argument is made available to any callables invoked by clearskies:
126
+ `sys_argv`. This contains `sys.argv`.
122
127
  """
123
128
 
124
129
  def __call__(self): # type: ignore
@@ -7,12 +7,12 @@ from typing import TYPE_CHECKING, Any, Callable
7
7
  from clearskies import exceptions
8
8
  from clearskies.di import Di
9
9
  from clearskies.di.additional_config import AdditionalConfig
10
+ from clearskies.input_outputs import InputOutput
10
11
  from clearskies.input_outputs import Programmatic
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from clearskies.endpoint import Endpoint
14
15
  from clearskies.endpoint_group import EndpointGroup
15
- from clearskies.input_outputs import InputOutput
16
16
 
17
17
 
18
18
  class Context:
@@ -54,6 +54,9 @@ class Context:
54
54
  self.application = application
55
55
 
56
56
  def execute_application(self, input_output: InputOutput):
57
+ self.di.add_binding("input_output", input_output)
58
+ self.di.add_class_override(InputOutput, input_output)
59
+
57
60
  if hasattr(self.application, "injectable_properties"):
58
61
  self.application.injectable_properties(self.di)
59
62
  return self.application(input_output)
@@ -72,6 +72,11 @@ class Wsgi(Context):
72
72
  by and large it's normal and expected that you'll persist the cache between requests by creating the context outside
73
73
  of any handler functions.
74
74
 
75
+ ### Context for Callables
76
+
77
+ When using this context, one additional named property becomes available to any callables invoked by clearskies:
78
+ `wsgi_environment`. This contains the environment object passed in by the WSGI server to clearskies.
79
+
75
80
  """
76
81
 
77
82
  def __call__(self, env, start_response): # type: ignore
clearskies/di/di.py CHANGED
@@ -520,7 +520,7 @@ class Di:
520
520
  """
521
521
  if not inspect.isclass(class_to_override):
522
522
  raise ValueError(
523
- "Invalid value passed to add_class_override for 'class_or_name' parameter: it was neither a name nor a class"
523
+ "Invalid value passed to add_class_override for 'class_to_override' parameter: it was not a class."
524
524
  )
525
525
 
526
526
  self._class_overrides_by_class[class_to_override] = replacement
@@ -535,8 +535,7 @@ class Di:
535
535
  override = self._class_overrides_by_class[object_to_override.__class__]
536
536
  if inspect.isclass(override):
537
537
  return self.build_class(override)
538
- if hasattr(override, "injectable_properties"):
539
- override.injectable_properties(self)
538
+ self.inject_properties(override.__class__)
540
539
  return override
541
540
 
542
541
  def add_override(self, name: str, replacement_class: type) -> None:
@@ -670,8 +669,7 @@ class Di:
670
669
  # ignore the first argument because that is just `self`
671
670
  build_arguments = init_args.args[1:]
672
671
  if not build_arguments:
673
- if hasattr(class_to_build, "injectable_properties"):
674
- class_to_build.injectable_properties(self)
672
+ self.inject_properties(class_to_build)
675
673
  built_value = class_to_build()
676
674
  if cache:
677
675
  self._prepared[class_to_build] = built_value # type: ignore
@@ -703,9 +701,8 @@ class Di:
703
701
 
704
702
  del self._building[class_id]
705
703
 
704
+ self.inject_properties(class_to_build)
706
705
  built_value = class_to_build(*args)
707
- if hasattr(built_value, "injectable_properties"):
708
- built_value.injectable_properties(self)
709
706
  if cache:
710
707
  self._prepared[class_to_build] = built_value # type: ignore
711
708
  return built_value
@@ -726,12 +723,18 @@ class Di:
726
723
  return None
727
724
  if not callable(class_to_build):
728
725
  return None
729
- if inspect.isabstract(class_to_build):
730
- return None
731
726
 
732
- # then first things first: check our class overrides
727
+ # check our class overrides
733
728
  if class_to_build in self._class_overrides_by_class:
734
- return self.build_class(self._class_overrides_by_class[class_to_build], context=context, cache=cache)
729
+ replacement = self._class_overrides_by_class[class_to_build]
730
+ if not inspect.isclass(replacement):
731
+ return replacement
732
+ return self.build_class(replacement, context=context, cache=cache)
733
+
734
+ # generally we can't build abstract classes, so if the class is abstract then we should pass.
735
+ # However, this is not the case if it has an override - then the developer has given us specific guidance
736
+ if inspect.isabstract(class_to_build):
737
+ return None
735
738
 
736
739
  # next check our additional config classes
737
740
  built_value = None
@@ -844,6 +847,23 @@ class Di:
844
847
  return False
845
848
  return True
846
849
 
850
+ def inject_properties(self, cls):
851
+ if hasattr(cls, "injectable_properties"):
852
+ cls.injectable_properties(self)
853
+ return
854
+
855
+ if not hasattr(cls, "__injectable_properties_sanity_check"):
856
+ return
857
+
858
+ for attribute_name in dir(cls):
859
+ attribute = getattr(cls, attribute_name)
860
+ if hasattr(attribute, "initiated_guard") and hasattr(attribute, "set_di"):
861
+ raise ValueError(
862
+ f"Class '{cls.__name__}' has an injectable property attached, but does not include clearskies.di.injectable_properties.InjectableProperties in it's parent classes. You must include this as a parent class."
863
+ )
864
+ cls.__injectable_properties_sanity_check = True
865
+ return
866
+
847
867
  def provide_di(self):
848
868
  return self
849
869
 
clearskies/end.py CHANGED
@@ -88,8 +88,6 @@ class End(
88
88
  if response:
89
89
  return response
90
90
 
91
- self.di.add_binding("input_output", input_output)
92
-
93
91
  # catch everything when we do an AuthN/AuthZ check because we allow custom-defined classes,
94
92
  # and this gives more flexibility (or possibly forgiveness) for how they raise exceptions.
95
93
  try:
clearskies/endpoint.py CHANGED
@@ -969,7 +969,7 @@ class Endpoint(
969
969
  """Whether or not we can handle an incoming request based on URL and request method."""
970
970
  # soo..... this excessively duplicates the logic in __call__, but I'm being lazy right now
971
971
  # and not fixing it.
972
- request_method = input_output.get_request_method().upper()
972
+ request_method = input_output.request_method.upper()
973
973
  if request_method == "OPTIONS":
974
974
  return True
975
975
  if request_method not in self.request_methods:
@@ -986,7 +986,7 @@ class Endpoint(
986
986
  # matches_request is only checked by the endpoint group, not by the context. As a result, we need to check our
987
987
  # route. However we always have to check our route anyway because the full routing data can only be figured
988
988
  # out at the endpoint level, so calling out to routing.mattch_route is unavoidable.
989
- request_method = input_output.get_request_method().upper()
989
+ request_method = input_output.request_method.upper()
990
990
  if request_method == "OPTIONS":
991
991
  return self.cors(input_output)
992
992
  if request_method not in self.request_methods:
@@ -5,19 +5,16 @@ import sys
5
5
  from os import isatty
6
6
  from sys import stdin
7
7
 
8
+ from clearskies.input_outputs.headers import Headers
8
9
  from clearskies.input_outputs.input_output import InputOutput
9
10
 
10
11
 
11
12
  class Cli(InputOutput):
12
- _args: list[str] = []
13
+ path: str
13
14
  _has_body: bool = False
14
15
  _body: str = ""
15
- _request_method: str = ""
16
- _request_headers: dict[str, str] = {}
17
16
 
18
17
  def __init__(self):
19
- self._request_headers = {}
20
- self._args = []
21
18
  self._parse_args(sys.argv)
22
19
  super().__init__()
23
20
 
@@ -30,16 +27,13 @@ class Cli(InputOutput):
30
27
  sys.exit(final)
31
28
  print(final)
32
29
 
33
- def get_arguments(self):
34
- return sys.argv
35
-
36
30
  def _parse_args(self, argv):
37
31
  tty_data = None
38
32
  if not isatty(stdin.fileno()):
39
33
  tty_data = sys.stdin.read().strip()
40
34
 
41
35
  request_headers = {}
42
- self._args = []
36
+ args = []
43
37
  kwargs = {}
44
38
  index = 0
45
39
  # In general we will use positional arguments for routing, and kwargs for request data.
@@ -49,10 +43,10 @@ class Cli(InputOutput):
49
43
  while index < len(argv) - 1:
50
44
  index += 1
51
45
 
52
- # if we don't start with a dash then we are a positional argument
46
+ # if we don't start with a dash then we are a positional argument which are used for building the URL-equivalent
53
47
  arg = argv[index]
54
48
  if arg[0] != "-":
55
- self._args.append(arg)
49
+ args.append(arg)
56
50
  continue
57
51
 
58
52
  # otherwise a kwarg
@@ -83,8 +77,8 @@ class Cli(InputOutput):
83
77
 
84
78
  kwargs[key] = value
85
79
 
86
- self._request_headers = request_headers
87
- self._request_method = "GET"
80
+ self.request_headers = Headers(request_headers)
81
+ self.request_method = "GET"
88
82
  request_method_source = ""
89
83
  for key in ["x", "X", "request_method"]:
90
84
  if key not in kwargs:
@@ -94,7 +88,7 @@ class Cli(InputOutput):
94
88
  raise ValueError(
95
89
  f"Invalid clearskies cli calling sequence: the request method was specified via both the -{key} parameter and the -{request_method_source} parameter. To avoid ambiguity, it should only be set once."
96
90
  )
97
- self._request_method = kwargs[key]
91
+ self.request_method = kwargs[key].upper()
98
92
  del kwargs[key]
99
93
  request_method_source = key
100
94
 
@@ -127,6 +121,8 @@ class Cli(InputOutput):
127
121
  final_data = kwargs
128
122
  data_source = "kwargs"
129
123
 
124
+ self.path = "/".join(args)
125
+
130
126
  # Most of the above inputs result in a string for our final data, in which case we'll leave it as the "raw body"
131
127
  # so that it can optionally be interpreted as JSON. If we received a bunch of kwargs though, we'll allow those to
132
128
  # only be "read" as JSON.
@@ -139,17 +135,8 @@ class Cli(InputOutput):
139
135
  self._has_body = True
140
136
  self._body = final_data
141
137
 
142
- def get_script_name(self):
143
- return sys.argv[0]
144
-
145
- def get_path_info(self):
146
- return "/".join(self._args)
147
-
148
138
  def get_full_path(self):
149
- return self.get_path_info()
150
-
151
- def get_request_method(self):
152
- return self._request_method
139
+ return self.path
153
140
 
154
141
  def has_body(self):
155
142
  return self._has_body
@@ -160,14 +147,11 @@ class Cli(InputOutput):
160
147
 
161
148
  return self._body
162
149
 
150
+ def get_protocol(self):
151
+ return "cli"
152
+
163
153
  def context_specifics(self):
164
- return {}
154
+ return {"sys_argv": sys.argv}
165
155
 
166
156
  def get_client_ip(self):
167
157
  return "127.0.0.1"
168
-
169
- def get_query_string(self):
170
- return ""
171
-
172
- def get_request_headers(self):
173
- return self._request_headers
@@ -5,6 +5,7 @@ import re
5
5
 
6
6
  class Headers:
7
7
  _headers: dict[str, str] = {}
8
+ _duck_cheat = "headers"
8
9
 
9
10
  def __init__(self, headers: dict[str, str] = {}):
10
11
  self.__dict__["_headers"] = (
@@ -5,7 +5,9 @@ from abc import ABC, abstractmethod
5
5
  from typing import TYPE_CHECKING, Any
6
6
  from urllib.parse import parse_qs
7
7
 
8
- from clearskies import configs, configurable, input_outputs
8
+ from clearskies import configs, configurable
9
+
10
+ from .headers import Headers
9
11
 
10
12
  if TYPE_CHECKING:
11
13
  from clearskies import typing
@@ -14,21 +16,18 @@ if TYPE_CHECKING:
14
16
  class InputOutput(ABC, configurable.Configurable):
15
17
  """Manage the request and response to the client."""
16
18
 
17
- response_headers: input_outputs.Headers = None # type: ignore
18
- request_headers: input_outputs.Headers = None # type: ignore
19
+ response_headers = configs.Headers(default=Headers())
20
+ request_headers = configs.Headers(default=Headers())
19
21
  query_parameters = configs.AnyDict(default={})
20
22
  routing_data = configs.StringDict(default={})
21
23
  authorization_data = configs.AnyDict(default={})
24
+ request_method = configs.Select(["GET", "POST", "PATCH", "OPTIONS", "DELETE", "SEARCH"], default="GET")
22
25
 
23
26
  _body_as_json: dict[str, Any] | list[Any] | None = {}
24
27
  _body_loaded_as_json = False
25
28
 
26
29
  def __init__(self):
27
- self.response_headers = input_outputs.Headers()
28
- self.request_headers = input_outputs.Headers(self.get_request_headers())
29
- self.query_parameters = {key: val[0] for (key, val) in parse_qs(self.get_query_string()).items()}
30
- self.authorization_data = {}
31
- self.routing_data = {}
30
+ self.response_headers = Headers()
32
31
  self.finalize_and_validate_configuration()
33
32
 
34
33
  @abstractmethod
@@ -65,41 +64,17 @@ class InputOutput(ABC, configurable.Configurable):
65
64
  self._body_as_json = None
66
65
  return self._body_as_json
67
66
 
68
- @abstractmethod
69
- def get_request_method(self) -> str:
70
- """Return the request method set by the client."""
71
- pass
72
-
73
- @abstractmethod
74
- def get_script_name(self) -> str:
75
- """Return the script name, e.g. the path requested."""
76
- pass
77
-
78
- @abstractmethod
79
- def get_path_info(self) -> str:
80
- """Return the path info for the request."""
81
- pass
82
-
83
- @abstractmethod
84
- def get_query_string(self) -> str:
85
- """Return the full query string for the request (everything after the first question mark in the document URL)."""
86
- pass
87
-
88
67
  @abstractmethod
89
68
  def get_client_ip(self):
90
69
  pass
91
70
 
92
71
  @abstractmethod
93
- def get_request_headers(self) -> dict[str, str]:
72
+ def get_protocol(self):
94
73
  pass
95
74
 
75
+ @abstractmethod
96
76
  def get_full_path(self) -> str:
97
- """Return the full path requested by the client."""
98
- path_info = self.get_path_info()
99
- script_name = self.get_script_name()
100
- if not path_info or path_info[0] != "/":
101
- path_info = f"/{path_info}"
102
- return f"{path_info}{script_name}".replace("//", "/")
77
+ pass
103
78
 
104
79
  def context_specifics(self):
105
80
  return {}
@@ -117,17 +92,19 @@ class InputOutput(ABC, configurable.Configurable):
117
92
 
118
93
  And this function returns a dictionary with the following values:
119
94
 
120
- | Key | Type | Ref | Value |
121
- |--------------------|----------------------------------|---------------------------------|---------------------------------------------------------------------------------|
122
- | routing_data | dict[str, str] | input_output.routing_data | A dictionary of data extracted from URL path parameters. |
123
- | authorization_data | dict[str, Any] | input_output.authorization_data | A dictionary containing the authorization data set by the authentication method |
124
- | request_data | dict[str, Any] | None | input_output.request_data | The data sent along with the request (assuming a JSON request body) |
125
- | query_parameters | dict[str, Any] | input_output.query_parameters | The query parameters |
126
- | request_headers | clearskies.input_outputs.Headers | input_output.request_headers | The request headers sent by the client |
127
- | **routing_data | string | **input_output.routing_data | The routing data is unpacked so keys can be fetched directly |
95
+ | Key | Type | Ref | Value |
96
+ |--------------------|----------------------------------|------------------------------------|---------------------------------------------------------------------------------|
97
+ | routing_data | dict[str, str] | input_output.routing_data | A dictionary of data extracted from URL path parameters. |
98
+ | authorization_data | dict[str, Any] | input_output.authorization_data | A dictionary containing the authorization data set by the authentication method |
99
+ | request_data | dict[str, Any] | None | input_output.request_data | The data sent along with the request (assuming a JSON request body) |
100
+ | query_parameters | dict[str, Any] | input_output.query_parameters | The query parameters |
101
+ | request_headers | clearskies.input_outputs.Headers | input_output.request_headers | The request headers sent by the client |
102
+ | **routing_data | string | **input_output.routing_data | The routing data is unpacked so keys can be fetched directly |
103
+ | **[varies] | varies | **input_output.context_specifics() | Any additional properties added on by the context (see your context docs) |
128
104
  """
129
105
  return {
130
106
  **self.routing_data,
107
+ **self.context_specifics(),
131
108
  **{
132
109
  "routing_data": self.routing_data,
133
110
  "authorization_data": self.authorization_data,
@@ -2,14 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
+ from clearskies.input_outputs.headers import Headers
5
6
  from clearskies.input_outputs.input_output import InputOutput
6
7
 
7
8
 
8
9
  class Programmatic(InputOutput):
9
10
  _body: str | dict[str, Any] | list[Any] = ""
10
- _request_method: str = ""
11
- _request_headers: dict[str, Any] = {}
12
11
  url: str = ""
12
+ ip_address: str = "127.0.0.1"
13
+ protocol: str = "https"
13
14
 
14
15
  def __init__(
15
16
  self,
@@ -18,35 +19,30 @@ class Programmatic(InputOutput):
18
19
  body: str | dict[str, Any] | list[Any] = "",
19
20
  query_parameters: dict[str, Any] = {},
20
21
  request_headers: dict[str, str] = {},
22
+ ip_address: str = "127.0.0.1",
23
+ protocol: str = "https",
21
24
  ):
22
25
  self.url = url
23
- self._request_headers = {**request_headers}
26
+ self.request_headers = Headers(request_headers)
27
+ self.query_parameters = query_parameters
28
+ self.ip_address = ip_address
29
+ self.protocol = protocol
24
30
  self._body_loaded_as_json = True
25
31
  self._body_as_json = None
26
- self._request_method = request_method
32
+ self.request_method = request_method
27
33
  if body:
28
34
  self._body = body
29
35
  if isinstance(body, dict) or isinstance(body, list):
30
36
  self._body_as_json = body
31
37
 
32
38
  super().__init__()
33
- self.query_parameters = {**query_parameters}
34
39
 
35
40
  def respond(self, response, status_code=200):
36
41
  return (status_code, response, self.response_headers)
37
42
 
38
- def get_script_name(self):
39
- return self.url
40
-
41
- def get_path_info(self):
42
- return self.url
43
-
44
43
  def get_full_path(self):
45
44
  return self.url
46
45
 
47
- def get_request_method(self):
48
- return self._request_method
49
-
50
46
  def has_body(self):
51
47
  return bool(self._body)
52
48
 
@@ -60,10 +56,7 @@ class Programmatic(InputOutput):
60
56
  return {}
61
57
 
62
58
  def get_client_ip(self):
63
- return "127.0.0.1"
64
-
65
- def get_query_string(self):
66
- return ""
59
+ return self.ip_address
67
60
 
68
- def get_request_headers(self):
69
- return self._request_headers
61
+ def get_protocol(self):
62
+ return self.protocol
@@ -3,22 +3,29 @@ from __future__ import annotations
3
3
  import json
4
4
  from typing import Callable
5
5
 
6
- from .input_output import InputOutput
6
+ from clearskies.input_outputs.headers import Headers
7
+ from clearskies.input_outputs.input_output import InputOutput
7
8
 
8
9
 
9
10
  class Wsgi(InputOutput):
10
11
  _environment: dict[str, str] = {}
11
- _start_response: Callable = None # type: ignore
12
- _request_headers: dict[str, str] = {}
12
+ _start_response: Callable
13
13
  _cached_body: str | None = None
14
14
 
15
15
  def __init__(self, environment, start_response):
16
16
  self._environment = environment
17
17
  self._start_response = start_response
18
- self._request_headers = {}
18
+ request_headers = {}
19
19
  for key, value in self._environment.items():
20
20
  if key.upper()[0:5] == "HTTP_":
21
- self._request_headers[key[5:].lower()] = value
21
+ request_headers[key[5:].lower()] = value
22
+
23
+ self.request_headers = Headers(request_headers)
24
+ self.query_parameters = {
25
+ key: val[0] for (key, val) in parse_qs(self._environment.get("QUERY_STRING", "")).items()
26
+ }
27
+ self.request_method = self._environment.get("REQUEST_METHOD").upper()
28
+
22
29
  super().__init__()
23
30
 
24
31
  def _from_environment(self, key):
@@ -49,20 +56,18 @@ class Wsgi(InputOutput):
49
56
  )
50
57
  return self._cached_body
51
58
 
52
- def get_request_method(self):
53
- return self._from_environment("REQUEST_METHOD").upper()
54
-
55
59
  def get_script_name(self):
56
60
  return self._from_environment("SCRIPT_NAME")
57
61
 
58
62
  def get_path_info(self):
59
63
  return self._from_environment("PATH_INFO")
60
64
 
61
- def get_query_string(self):
62
- return self._from_environment("QUERY_STRING")
63
-
64
- def get_content_type(self):
65
- return self._from_environment("CONTENT_TYPE")
65
+ def get_full_path(self):
66
+ path_info = self.get_path_info()
67
+ script_name = self.get_script_name()
68
+ if not path_info or path_info[0] != "/":
69
+ path_info = f"/{path_info}"
70
+ return f"{path_info}{script_name}".replace("//", "/")
66
71
 
67
72
  def get_protocol(self):
68
73
  return self._from_environment("wsgi.url_scheme").lower()
@@ -72,6 +77,3 @@ class Wsgi(InputOutput):
72
77
 
73
78
  def get_client_ip(self):
74
79
  return self._environment.get("REMOTE_ADDR")
75
-
76
- def get_request_headers(self):
77
- return self._request_headers
@@ -4,9 +4,10 @@ from abc import ABC
4
4
  from typing import Any
5
5
 
6
6
  import clearskies.configurable
7
+ from clearskies.di.injectable_properties import InjectableProperties
7
8
 
8
9
 
9
- class Secrets(ABC, clearskies.configurable.Configurable):
10
+ class Secrets(ABC, clearskies.configurable.Configurable, InjectableProperties):
10
11
  def create(self, path: str, value: str) -> bool:
11
12
  raise NotImplementedError(
12
13
  "It looks like you tried to use the secret system in clearskies, but didn't specify a secret manager."