Răsfoiți Sursa

Make `sydent.sms` pass `mypy --strict` (#429)

Co-authored-by: Brendan Abolivier <babolivier@matrix.org>
David Robertson 2 ani în urmă
părinte
comite
f939a2226b
5 a modificat fișierele cu 72 adăugiri și 9 ștergeri
  1. 1 0
      changelog.d/429.misc
  2. 1 0
      pyproject.toml
  3. 1 1
      sydent/http/httpclient.py
  4. 16 8
      sydent/sms/openmarket.py
  5. 53 0
      sydent/sms/types.py

+ 1 - 0
changelog.d/429.misc

@@ -0,0 +1 @@
+Make `sydent.sms` pass `mypy --strict`.

+ 1 - 0
pyproject.toml

@@ -51,6 +51,7 @@ files = [
     #     find sydent tests -type d -not -name __pycache__ -exec bash -c "mypy --strict '{}' > /dev/null"  \; -print
     "sydent/config",
     "sydent/db",
+    "sydent/sms",
     "sydent/terms",
     "sydent/threepid",
     "sydent/users",

+ 1 - 1
sydent/http/httpclient.py

@@ -86,7 +86,7 @@ class HTTPClient:
     async def post_json_maybe_get_json(
         self,
         uri: str,
-        post_json: Dict[Any, Any],
+        post_json: Dict[str, Any],
         opts: Dict[str, Any],
         max_size: Optional[int] = None,
     ) -> Tuple[IResponse, Optional[JsonDict]]:

+ 16 - 8
sydent/sms/openmarket.py

@@ -14,11 +14,13 @@
 
 import logging
 from base64 import b64encode
-from typing import TYPE_CHECKING, Dict, Optional
+from typing import TYPE_CHECKING, Dict, Optional, cast
 
 from twisted.web.http_headers import Headers
 
 from sydent.http.httpclient import SimpleHttpClient
+from sydent.sms.types import SendSMSBody, TypeOfNumber
+from sydent.types import JsonDict
 
 if TYPE_CHECKING:
     from sydent.sydent import Sydent
@@ -33,14 +35,14 @@ API_BASE_URL = "https://smsc.openmarket.com/sms/v4/mt"
 # API_BASE_URL = "http://smsc-cie.openmarket.com/sms/v4/mt"
 
 # The TON (ie. Type of Number) codes by type used in our config file
-TONS = {
+TONS: Dict[str, TypeOfNumber] = {
     "long": 1,
     "short": 3,
     "alpha": 5,
 }
 
 
-def tonFromType(t: str) -> int:
+def tonFromType(t: str) -> TypeOfNumber:
     """
     Get the type of number from the originator's type.
 
@@ -69,7 +71,7 @@ class OpenMarketSMS:
         :param body: The message to send.
         :param dest: The destination MSISDN to send the text message to.
         """
-        send_body = {
+        send_body: SendSMSBody = {
             "mobileTerminate": {
                 "message": {"content": body, "type": "text"},
                 "destination": {
@@ -94,8 +96,14 @@ class OpenMarketSMS:
             }
         )
 
-        resp, body = await self.http_cli.post_json_maybe_get_json(
-            API_BASE_URL, send_body, {"headers": req_headers}
+        # Type safety: The case from a TypedDict to a regular Dict is required
+        # because the two are deliberately not compatible. See
+        #    https://github.com/python/mypy/issues/4976
+        # for details, but in a nutshell: Dicts can have keys added or removed,
+        # and that would break the invariants that a TypedDict is there to check.
+        # The case below is safe because we never use send_body afterwards.
+        resp, response_body = await self.http_cli.post_json_maybe_get_json(
+            API_BASE_URL, cast(JsonDict, send_body), {"headers": req_headers}
         )
 
         headers = dict(resp.headers.getAllRawHeaders())
@@ -111,7 +119,7 @@ class OpenMarketSMS:
         # Relevant OpenMarket API documentation:
         # https://www.openmarket.com/docs/Content/apis/v4http/send-json.htm
         if resp.code < 200 or resp.code >= 300:
-            if body is None or "error" not in body:
+            if response_body is None or "error" not in response_body:
                 raise Exception(
                     "OpenMarket API responded with status %d (request ID: %s)"
                     % (
@@ -120,7 +128,7 @@ class OpenMarketSMS:
                     ),
                 )
 
-            error = body["error"]
+            error = response_body["error"]
             raise Exception(
                 "OpenMarket API responded with status %d (request ID: %s): %s"
                 % (

+ 53 - 0
sydent/sms/types.py

@@ -0,0 +1,53 @@
+# See "Request body" section of
+# https://www.openmarket.com/docs/Content/apis/v4http/send-json.htm
+from typing_extensions import Literal, TypedDict
+
+TypeOfNumber = Literal[1, 3, 5]
+
+
+class SendSMSBody(TypedDict):
+    mobileTerminate: "MobileTerminate"
+
+
+class MobileTerminateRequired(TypedDict):
+    # OpenMarket says these are required fields
+    destination: "Destination"
+    message: "Message"
+
+
+class MobileTerminate(MobileTerminateRequired, total=False):
+    # And these are all optional.
+    interaction: Literal["one-way", "two-way"]
+    promotional: bool  # Ignored, unless we're sending to India
+    source: "Source"
+    # The API also offers optional "options" and "delivery" keys,
+    # which we don't use
+
+
+class MessageRequired(TypedDict):
+    type: Literal["text", "hexEncodedText", "binary", "wapPush"]
+    content: str
+
+
+class Message(MessageRequired, total=False):
+    charset: Literal["GSM", "Latin-1", "UTF-8", "UTF-16"]
+    validityPeriod: int
+    url: str
+    mlc: Literal["reject", "truncate", "segment"]
+    udh: bool
+
+
+class DestinationRequired(TypedDict):
+    address: str
+
+
+class Destination(DestinationRequired, total=False):
+    mobileOperatorId: int
+
+
+class SourceRequired(TypedDict):
+    address: str
+
+
+class Source(SourceRequired, total=False):
+    ton: TypeOfNumber