diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/_generated_types.py b/deltachat-rpc-client/src/deltachat_rpc_client/_generated_types.py new file mode 100644 index 0000000000..6a39e05888 --- /dev/null +++ b/deltachat-rpc-client/src/deltachat_rpc_client/_generated_types.py @@ -0,0 +1,775 @@ +from dataclasses import dataclass +from enum import Enum +from typing import TypeAlias, Union, Optional, Tuple, Any + + +class AccountEnum: + @dataclass(kw_only=True) + class Configured: + kind: str = "Configured" + addr: Optional[str] + color: str + display_name: Optional[str] + id: int + profile_image: Optional[str] + + @dataclass(kw_only=True) + class Unconfigured: + kind: str = "Unconfigured" + id: int + + +Account: TypeAlias = AccountEnum.Configured | AccountEnum.Unconfigured + + +@dataclass(kw_only=True) +class BasicChat: + archived: bool + chat_type: int + color: str + id: int + is_contact_request: bool + is_device_chat: bool + is_muted: bool + is_protected: bool + is_self_talk: bool + is_unpromoted: bool + name: str + profile_image: str + + +class ChatListItemFetchResultEnum: + @dataclass(kw_only=True) + class ChatListItem: + kind: str = "ChatListItem" + avatar_path: Optional[str] + color: str + dm_chat_contact: Optional[int] + fresh_message_counter: int + id: int + is_archived: bool + is_broadcast: bool + is_contact_request: bool + is_device_talk: bool + is_group: bool + is_muted: bool + is_pinned: bool + is_protected: bool + is_self_in_group: bool + is_self_talk: bool + is_sending_location: bool + last_message_id: Optional[int] + last_message_type: Optional["Viewtype"] + last_updated: Optional[int] + name: str + summary_preview_image: Optional[str] + summary_status: int + summary_text1: str + summary_text2: str + was_seen_recently: bool + + @dataclass(kw_only=True) + class ArchiveLink: + kind: str = "ArchiveLink" + fresh_message_counter: int + + @dataclass(kw_only=True) + class Error: + kind: str = "Error" + error: str + id: int + + +ChatListItemFetchResult: TypeAlias = ( + ChatListItemFetchResultEnum.ChatListItem + | ChatListItemFetchResultEnum.ArchiveLink + | ChatListItemFetchResultEnum.Error +) + + +class ChatVisibility(Enum): + NORMAL = "Normal" + ARCHIVED = "Archived" + PINNED = "Pinned" + + +@dataclass(kw_only=True) +class Contact: + address: str + auth_name: str + color: str + display_name: str + id: int + is_blocked: bool + is_verified: bool + last_seen: int + name: str + name_and_addr: str + profile_image: str + status: str + verifier_addr: str + verifier_id: int + was_seen_recently: bool + + +class DownloadState(Enum): + DONE = "Done" + AVAILABLE = "Available" + FAILURE = "Failure" + IN_PROGRESS = "InProgress" + + +@dataclass(kw_only=True) +class Event: + context_id: int + event: "EventType" + + +class EventTypeEnum: + @dataclass(kw_only=True) + class Info: + kind: str = "Info" + msg: str + + @dataclass(kw_only=True) + class SmtpConnected: + kind: str = "SmtpConnected" + msg: str + + @dataclass(kw_only=True) + class ImapConnected: + kind: str = "ImapConnected" + msg: str + + @dataclass(kw_only=True) + class SmtpMessageSent: + kind: str = "SmtpMessageSent" + msg: str + + @dataclass(kw_only=True) + class ImapMessageDeleted: + kind: str = "ImapMessageDeleted" + msg: str + + @dataclass(kw_only=True) + class ImapMessageMoved: + kind: str = "ImapMessageMoved" + msg: str + + @dataclass(kw_only=True) + class ImapInboxIdle: + kind: str = "ImapInboxIdle" + + @dataclass(kw_only=True) + class NewBlobFile: + kind: str = "NewBlobFile" + file: str + + @dataclass(kw_only=True) + class DeletedBlobFile: + kind: str = "DeletedBlobFile" + file: str + + @dataclass(kw_only=True) + class Warning: + kind: str = "Warning" + msg: str + + @dataclass(kw_only=True) + class Error: + kind: str = "Error" + msg: str + + @dataclass(kw_only=True) + class ErrorSelfNotInGroup: + kind: str = "ErrorSelfNotInGroup" + msg: str + + @dataclass(kw_only=True) + class MsgsChanged: + kind: str = "MsgsChanged" + chat_id: int + msg_id: int + + @dataclass(kw_only=True) + class ReactionsChanged: + kind: str = "ReactionsChanged" + chat_id: int + contact_id: int + msg_id: int + + @dataclass(kw_only=True) + class IncomingMsg: + kind: str = "IncomingMsg" + chat_id: int + msg_id: int + + @dataclass(kw_only=True) + class IncomingMsgBunch: + kind: str = "IncomingMsgBunch" + msg_ids: list[int] + + @dataclass(kw_only=True) + class MsgsNoticed: + kind: str = "MsgsNoticed" + chat_id: int + + @dataclass(kw_only=True) + class MsgDelivered: + kind: str = "MsgDelivered" + chat_id: int + msg_id: int + + @dataclass(kw_only=True) + class MsgFailed: + kind: str = "MsgFailed" + chat_id: int + msg_id: int + + @dataclass(kw_only=True) + class MsgRead: + kind: str = "MsgRead" + chat_id: int + msg_id: int + + @dataclass(kw_only=True) + class MsgDeleted: + kind: str = "MsgDeleted" + chat_id: int + msg_id: int + + @dataclass(kw_only=True) + class ChatModified: + kind: str = "ChatModified" + chat_id: int + + @dataclass(kw_only=True) + class ChatEphemeralTimerModified: + kind: str = "ChatEphemeralTimerModified" + chat_id: int + timer: int + + @dataclass(kw_only=True) + class ContactsChanged: + kind: str = "ContactsChanged" + contact_id: Optional[int] + + @dataclass(kw_only=True) + class LocationChanged: + kind: str = "LocationChanged" + contact_id: Optional[int] + + @dataclass(kw_only=True) + class ConfigureProgress: + kind: str = "ConfigureProgress" + comment: Optional[str] + progress: int + + @dataclass(kw_only=True) + class ImexProgress: + kind: str = "ImexProgress" + progress: int + + @dataclass(kw_only=True) + class ImexFileWritten: + kind: str = "ImexFileWritten" + path: str + + @dataclass(kw_only=True) + class SecurejoinInviterProgress: + kind: str = "SecurejoinInviterProgress" + contact_id: int + progress: int + + @dataclass(kw_only=True) + class SecurejoinJoinerProgress: + kind: str = "SecurejoinJoinerProgress" + contact_id: int + progress: int + + @dataclass(kw_only=True) + class ConnectivityChanged: + kind: str = "ConnectivityChanged" + + @dataclass(kw_only=True) + class SelfavatarChanged: + kind: str = "SelfavatarChanged" + + @dataclass(kw_only=True) + class WebxdcStatusUpdate: + kind: str = "WebxdcStatusUpdate" + msg_id: int + status_update_serial: int + + @dataclass(kw_only=True) + class WebxdcInstanceDeleted: + kind: str = "WebxdcInstanceDeleted" + msg_id: int + + +EventType: TypeAlias = ( + EventTypeEnum.Info + | EventTypeEnum.SmtpConnected + | EventTypeEnum.ImapConnected + | EventTypeEnum.SmtpMessageSent + | EventTypeEnum.ImapMessageDeleted + | EventTypeEnum.ImapMessageMoved + | EventTypeEnum.ImapInboxIdle + | EventTypeEnum.NewBlobFile + | EventTypeEnum.DeletedBlobFile + | EventTypeEnum.Warning + | EventTypeEnum.Error + | EventTypeEnum.ErrorSelfNotInGroup + | EventTypeEnum.MsgsChanged + | EventTypeEnum.ReactionsChanged + | EventTypeEnum.IncomingMsg + | EventTypeEnum.IncomingMsgBunch + | EventTypeEnum.MsgsNoticed + | EventTypeEnum.MsgDelivered + | EventTypeEnum.MsgFailed + | EventTypeEnum.MsgRead + | EventTypeEnum.MsgDeleted + | EventTypeEnum.ChatModified + | EventTypeEnum.ChatEphemeralTimerModified + | EventTypeEnum.ContactsChanged + | EventTypeEnum.LocationChanged + | EventTypeEnum.ConfigureProgress + | EventTypeEnum.ImexProgress + | EventTypeEnum.ImexFileWritten + | EventTypeEnum.SecurejoinInviterProgress + | EventTypeEnum.SecurejoinJoinerProgress + | EventTypeEnum.ConnectivityChanged + | EventTypeEnum.SelfavatarChanged + | EventTypeEnum.WebxdcStatusUpdate + | EventTypeEnum.WebxdcInstanceDeleted +) + + +@dataclass(kw_only=True) +class FullChat: + archived: bool + can_send: bool + chat_type: int + color: str + contact_ids: list[int] + contacts: list["Contact"] + ephemeral_timer: int + fresh_message_counter: int + id: int + is_contact_request: bool + is_device_chat: bool + is_muted: bool + is_protected: bool + is_self_talk: bool + is_unpromoted: bool + mailing_list_address: str + name: str + profile_image: str + self_in_group: bool + was_seen_recently: bool + + +@dataclass(kw_only=True) +class HttpResponse: + blob: str + encoding: str + mimetype: str + + +@dataclass(kw_only=True) +class Location: + accuracy: float + chat_id: int + contact_id: int + is_independent: bool + latitude: float + location_id: int + longitude: float + marker: str + msg_id: int + timestamp: int + + +@dataclass(kw_only=True) +class Message: + chat_id: int + dimensions_height: int + dimensions_width: int + download_state: "DownloadState" + duration: int + error: str + file: str + file_bytes: int + file_mime: str + file_name: str + from_id: int + has_deviating_timestamp: bool + has_html: bool + has_location: bool + id: int + is_bot: bool + is_forwarded: bool + is_info: bool + is_setupmessage: bool + override_sender_name: str + parent_id: int + quote: Optional["MessageQuote"] + reactions: Optional["Reactions"] + received_timestamp: int + sender: "Contact" + setup_code_begin: str + show_padlock: bool + sort_timestamp: int + state: int + subject: str + system_message_type: "SystemMessageType" + text: str + timestamp: int + videochat_type: int + videochat_url: str + view_type: "Viewtype" + webxdc_info: Optional["WebxdcMessageInfo"] + + +@dataclass(kw_only=True) +class MessageData: + file: str + html: str + location: Tuple[float, float] + override_sender_name: str + quoted_message_id: int + text: str + viewtype: Optional["Viewtype"] + + +class MessageListItemEnum: + @dataclass(kw_only=True) + class Message: + kind: str = "Message" + msg_id: int + + @dataclass(kw_only=True) + class DayMarker: + kind: str = "DayMarker" + timestamp: int + + +MessageListItem: TypeAlias = MessageListItemEnum.Message | MessageListItemEnum.DayMarker + + +class MessageLoadResultEnum: + @dataclass(kw_only=True) + class Message: + kind: str = "Message" + chat_id: int + dimensions_height: int + dimensions_width: int + download_state: "DownloadState" + duration: int + error: Optional[str] + file: Optional[str] + file_bytes: int + file_mime: Optional[str] + file_name: Optional[str] + from_id: int + has_deviating_timestamp: bool + has_html: bool + has_location: bool + id: int + is_bot: bool + is_forwarded: bool + is_info: bool + is_setupmessage: bool + override_sender_name: Optional[str] + parent_id: Optional[int] + quote: Optional["MessageQuote"] + reactions: Optional["Reactions"] + received_timestamp: int + sender: "Contact" + setup_code_begin: Optional[str] + show_padlock: bool + sort_timestamp: int + state: int + subject: str + system_message_type: "SystemMessageType" + text: str + timestamp: int + videochat_type: Optional[int] + videochat_url: Optional[str] + view_type: "Viewtype" + webxdc_info: Optional["WebxdcMessageInfo"] + + @dataclass(kw_only=True) + class LoadingError: + kind: str = "LoadingError" + error: str + + +MessageLoadResult: TypeAlias = MessageLoadResultEnum.Message | MessageLoadResultEnum.LoadingError + + +@dataclass(kw_only=True) +class MessageNotificationInfo: + account_id: int + chat_id: int + chat_name: str + chat_profile_image: str + id: int + image: str + image_mime_type: str + summary_prefix: str + summary_text: str + + +class MessageQuoteEnum: + @dataclass(kw_only=True) + class JustText: + kind: str = "JustText" + text: str + + @dataclass(kw_only=True) + class WithMessage: + kind: str = "WithMessage" + author_display_color: str + author_display_name: str + image: Optional[str] + is_forwarded: bool + message_id: int + override_sender_name: Optional[str] + text: str + view_type: "Viewtype" + + +MessageQuote: TypeAlias = MessageQuoteEnum.JustText | MessageQuoteEnum.WithMessage + + +@dataclass(kw_only=True) +class MessageReadReceipt: + contact_id: int + timestamp: int + + +@dataclass(kw_only=True) +class MessageSearchResult: + author_color: str + author_id: int + author_name: str + author_profile_image: str + chat_color: str + chat_name: str + chat_profile_image: str + chat_type: int + id: int + is_chat_archived: bool + is_chat_contact_request: bool + is_chat_protected: bool + message: str + timestamp: int + + +class MuteDurationEnum: + @dataclass(kw_only=True) + class NotMuted: + kind: str = "NotMuted" + + @dataclass(kw_only=True) + class Forever: + kind: str = "Forever" + + @dataclass(kw_only=True) + class Until: + kind: str = "Until" + timestamp: int + + +MuteDuration: TypeAlias = MuteDurationEnum.NotMuted | MuteDurationEnum.Forever | MuteDurationEnum.Until + + +@dataclass(kw_only=True) +class ProviderInfo: + before_login_hint: str + overview_page: str + status: int + + +class QrEnum: + @dataclass(kw_only=True) + class AskVerifyContact: + kind: str = "AskVerifyContact" + authcode: str + contact_id: int + fingerprint: str + invitenumber: str + + @dataclass(kw_only=True) + class AskVerifyGroup: + kind: str = "AskVerifyGroup" + authcode: str + contact_id: int + fingerprint: str + grpid: str + grpname: str + invitenumber: str + + @dataclass(kw_only=True) + class FprOk: + kind: str = "FprOk" + contact_id: int + + @dataclass(kw_only=True) + class FprMismatch: + kind: str = "FprMismatch" + contact_id: Optional[int] + + @dataclass(kw_only=True) + class FprWithoutAddr: + kind: str = "FprWithoutAddr" + fingerprint: str + + @dataclass(kw_only=True) + class Account: + kind: str = "Account" + domain: str + + @dataclass(kw_only=True) + class Backup: + kind: str = "Backup" + ticket: str + + @dataclass(kw_only=True) + class WebrtcInstance: + kind: str = "WebrtcInstance" + domain: str + instance_pattern: str + + @dataclass(kw_only=True) + class Addr: + kind: str = "Addr" + contact_id: int + draft: Optional[str] + + @dataclass(kw_only=True) + class Url: + kind: str = "Url" + url: str + + @dataclass(kw_only=True) + class Text: + kind: str = "Text" + text: str + + @dataclass(kw_only=True) + class WithdrawVerifyContact: + kind: str = "WithdrawVerifyContact" + authcode: str + contact_id: int + fingerprint: str + invitenumber: str + + @dataclass(kw_only=True) + class WithdrawVerifyGroup: + kind: str = "WithdrawVerifyGroup" + authcode: str + contact_id: int + fingerprint: str + grpid: str + grpname: str + invitenumber: str + + @dataclass(kw_only=True) + class ReviveVerifyContact: + kind: str = "ReviveVerifyContact" + authcode: str + contact_id: int + fingerprint: str + invitenumber: str + + @dataclass(kw_only=True) + class ReviveVerifyGroup: + kind: str = "ReviveVerifyGroup" + authcode: str + contact_id: int + fingerprint: str + grpid: str + grpname: str + invitenumber: str + + @dataclass(kw_only=True) + class Login: + kind: str = "Login" + address: str + + +Qr: TypeAlias = ( + QrEnum.AskVerifyContact + | QrEnum.AskVerifyGroup + | QrEnum.FprOk + | QrEnum.FprMismatch + | QrEnum.FprWithoutAddr + | QrEnum.Account + | QrEnum.Backup + | QrEnum.WebrtcInstance + | QrEnum.Addr + | QrEnum.Url + | QrEnum.Text + | QrEnum.WithdrawVerifyContact + | QrEnum.WithdrawVerifyGroup + | QrEnum.ReviveVerifyContact + | QrEnum.ReviveVerifyGroup + | QrEnum.Login +) + + +@dataclass(kw_only=True) +class Reaction: + count: int + emoji: str + is_from_self: bool + + +@dataclass(kw_only=True) +class Reactions: + reactions: list["Reaction"] + reactions_by_contact: dict[Any, list[str]] + + +class SystemMessageType(Enum): + UNKNOWN = "Unknown" + GROUP_NAME_CHANGED = "GroupNameChanged" + GROUP_IMAGE_CHANGED = "GroupImageChanged" + MEMBER_ADDED_TO_GROUP = "MemberAddedToGroup" + MEMBER_REMOVED_FROM_GROUP = "MemberRemovedFromGroup" + AUTOCRYPT_SETUP_MESSAGE = "AutocryptSetupMessage" + SECUREJOIN_MESSAGE = "SecurejoinMessage" + LOCATION_STREAMING_ENABLED = "LocationStreamingEnabled" + LOCATION_ONLY = "LocationOnly" + CHAT_PROTECTION_ENABLED = "ChatProtectionEnabled" + CHAT_PROTECTION_DISABLED = "ChatProtectionDisabled" + WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate" + EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged" + MULTI_DEVICE_SYNC = "MultiDeviceSync" + WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage" + + +class Viewtype(Enum): + UNKNOWN = "Unknown" + TEXT = "Text" + IMAGE = "Image" + GIF = "Gif" + STICKER = "Sticker" + AUDIO = "Audio" + VOICE = "Voice" + VIDEO = "Video" + FILE = "File" + VIDEOCHAT_INVITATION = "VideochatInvitation" + WEBXDC = "Webxdc" + + +@dataclass(kw_only=True) +class WebxdcMessageInfo: + document: str + icon: str + internet_access: bool + name: str + source_code_url: str + summary: str diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 20fc11b366..a8d0ce0e6f 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union +from ._generated_types import MuteDurationEnum from ._utils import AttrDict from .const import ChatVisibility, ViewType from .contact import Contact @@ -54,14 +55,14 @@ def mute(self, duration: Optional[int] = None) -> None: """ if duration is not None: assert duration > 0, "Invalid duration" - dur: dict = {"kind": "Until", "duration": duration} + dur: dict = MuteDurationEnum.Until(duration=duration) else: - dur = {"kind": "Forever"} + dur = MuteDurationEnum.Forever() self._rpc.set_chat_mute_duration(self.account.id, self.id, dur) def unmute(self) -> None: """Unmute this chat.""" - self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"}) + self._rpc.set_chat_mute_duration(self.account.id, self.id, MuteDurationEnum.NotMuted()) def pin(self) -> None: """Pin this chat.""" diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index 4a181c528c..e716352c4c 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -1,3 +1,4 @@ +import dataclasses import json import logging import os @@ -12,6 +13,13 @@ class JsonRpcError(Exception): pass +class DataclassEncoder(json.JSONEncoder): + def default(self, obj): + if dataclasses.is_dataclass(obj) and not isinstance(obj, type): + return dataclasses.asdict(obj) + return json.JSONEncoder.default(self, obj) + + class Rpc: def __init__(self, accounts_dir: Optional[str] = None, **kwargs): """The given arguments will be passed to subprocess.Popen()""" @@ -109,7 +117,7 @@ def writer_loop(self) -> None: request = self.request_queue.get() if not request: break - data = (json.dumps(request) + "\n").encode() + data = (json.dumps(request, cls=DataclassEncoder) + "\n").encode() self.process.stdin.write(data) self.process.stdin.flush() diff --git a/scripts/generate_openrpc_bindings.py b/scripts/generate_openrpc_bindings.py new file mode 100755 index 0000000000..13a73cb61c --- /dev/null +++ b/scripts/generate_openrpc_bindings.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +import subprocess +import json +from pprint import pprint + + +def from_camel_case(name): + """Convert a camelCase identifier to snake case.""" + l = len(name) + name += "X" + res = "" + start = 0 + for i in range(len(name)): + if i > 0 and name[i].isupper(): + res += name[start:i].lower() + if i != l: + res += "_" + start = i + return res + + +def generate_method(method): + assert method["paramStructure"] == "by-position" + name = method["name"] + params = method["params"] + args_typed = ", ".join( + [ + from_camel_case(param["name"]) + + ": " + + (decode_type(param["schema"]) or "Any") + for param in params + ] + ) + args = ", ".join([from_camel_case(param["name"]) for param in params]) + result_type = decode_type(method["result"]["schema"]) + print(f"def {name}({args_typed}) -> {result_type}:") + if "description" in method: + description = method["description"] + if "\n" in description: + print(f' """{method["description"].lstrip()}\n """') + else: + print(f' """{method["description"].lstrip()}"""') + print(f" rpc_call({args})") + print() + + +def generate_openrpc_methods(openrpc_spec): + for method in openrpc_spec["methods"]: + generate_method(method) + + +def decode_type(property_desc): + if "anyOf" in property_desc: + t = property_desc["anyOf"] + assert len(t) == 2 + assert t[1] == {"type": "null"} + ref = t[0]["$ref"] + assert ref.startswith("#/components/schemas/") + return f'Optional["{ref.removeprefix("#/components/schemas/")}"]' + elif "$ref" in property_desc: + t = property_desc["$ref"] + assert t.startswith("#/components/schemas/") + t = t.removeprefix("#/components/schemas/") + return f'"{t}"' + elif property_desc["type"] == "null": + return "None" + elif "null" in property_desc["type"]: + assert len(property_desc["type"]) == 2 + assert property_desc["type"][1] == "null" + property_desc["type"] = property_desc["type"][0] + t = decode_type(property_desc) + if t: + return f"Optional[{t}]" + elif property_desc["type"] == "boolean": + return "bool" + elif property_desc["type"] == "integer": + return "int" + elif property_desc["type"] == "number" and property_desc["format"] == "double": + return "float" + elif property_desc["type"] == "string": + return "str" + elif property_desc["type"] == "array": + if isinstance(property_desc["items"], list): + items_desc = ", ".join(decode_type(x) for x in property_desc["items"]) + return f"Tuple[{items_desc}]" + else: + items_type = decode_type(property_desc["items"]) + return f"list[{items_type}]" + elif "additionalProperties" in property_desc: + additional_properties = property_desc["additionalProperties"] + return f"dict[Any, {decode_type(additional_properties)}]" + return None + + +def generate_variant(variant) -> str: + """Prints generated type for enum variant. + + Returns the name of the generated type. + """ + assert variant["type"] == "object" + kind = variant["properties"]["kind"] + assert kind["type"] == "string" + assert len(kind["enum"]) == 1 + kind_name = kind["enum"][0] + kind_name = kind_name[0].upper() + kind_name[1:] + + print(f" @dataclass(kw_only=True)") + print(f" class {kind_name}:") + print(f" kind: str = \"{kind_name}\"") + for property_name, property_desc in variant["properties"].items(): + property_name = from_camel_case(property_name) + if property_name == "kind": + continue + if t := decode_type(property_desc): + print(f" {property_name}: {t}") + else: + print("# TODO") + pprint(property_name) + pprint(property_desc) + print() + + return kind_name + + +def generate_type(type_name, schema): + if "oneOf" in schema: + if all(x["type"] == "string" for x in schema["oneOf"]): + # Simple enumeration consisting only of various string types. + print(f"class {type_name}(Enum):") + for x in schema["oneOf"]: + for e in x["enum"]: + print(f' {from_camel_case(e).upper()} = "{e}"') + else: + # Union type. + namespace = f"{type_name}Enum" + print(f"class {namespace}:") + kind_names = [f"{namespace}.{generate_variant(x)}" for x in schema["oneOf"]] + + print(f"{type_name}: TypeAlias = {' | '.join(kind_names)}") + elif schema["type"] == "string": + print(f"class {type_name}(Enum):") + for e in schema["enum"]: + print(f' {from_camel_case(e).upper()} = "{e}"') + else: + print("@dataclass(kw_only=True)") + print(f"class {type_name}:") + for property_name, property_desc in schema["properties"].items(): + property_name = from_camel_case(property_name) + if decode_type(property_desc): + print(f" {property_name}: {decode_type(property_desc)}") + else: + print(f"# TODO {property_name}") + pprint(property_desc) + + print() + + +def generate_openrpc_types(openrpc_spec): + for type_name, schema in openrpc_spec["components"]["schemas"].items(): + generate_type(type_name, schema) + + +def main(): + openrpc_spec = json.loads( + subprocess.run( + ["deltachat-rpc-server", "--openrpc"], capture_output=True + ).stdout + ) + print("from dataclasses import dataclass") + print("from enum import Enum") + print("from typing import TypeAlias, Union, Optional, Tuple, Any") + generate_openrpc_types(openrpc_spec) + generate_openrpc_methods(openrpc_spec) + + +if __name__ == "__main__": + main()