ahk python interop via COM server - minimal working example

Post your working scripts, libraries and tools.
bonobo
Posts: 76
Joined: 03 Sep 2023, 20:13

ahk python interop via COM server - minimal working example

Post by bonobo » 23 Nov 2023, 22:03

This is mostly based on @MrDoge's tutorial posted at
https://stackoverflow.com/questions/65780086/how-to-program-hotstrings-in-python-like-in-autohotkey/65783573#65783573

Usage Examples:
calling custom-defined functions:

Code: Select all

msgbox py("uuid_2_shortid", "ef0cbdd0-b71d-446e-8498-745a95606fc9") ; ⟹ "0L0M7x23bkSEmHRalWBvyQ"
msgbox py("shortid_2_uuid", "0L0M7x23bkSEmHRalWBvyQ") ; ⟹ "ef0cbdd0-b71d-446e-8498-745a95606fc9"
msgbox ticks := py("time_now_in_ticks") ; ⟹ 638363906753198976
msgbox py("ticks_to_readable_time", ticks) ; ⟹ "2023-11-24 02:45:24.107276"
evaluating expressions:

Code: Select all

msgbox py("eval", "[x**2 for x in range(10)]")[3] ; ⟹ 9
msgbox py("eval", "(lambda radius: 3.14159 * radius ** 2)(5)") ; ⟹ 78.935749999
executing statements (returning local variables as JSON): 1. math operations

Code: Select all

msgbox py("exec", statements := "
(
import math
sin_pi = math.sin(math.pi)
log_e = math.log(math.e)
sqrt_of_16 = math.sqrt(16)
factorial_of_5 = math.factorial(5)
)", 1)["factorial_of_5"] ; ⟹ 120

msgbox py("exec", statements := "
(
import random
result = {
    'random_int': random.randint(1, 100),
    'random_float': random.random(),
    'random_choice': random.choice(['apple', 'banana', 'cherry', 'date'])
}
)", 1)["result"]["random_choice"] ; ⟹ (e.g.) "banana"
executing statements (returning local variables as JSON): 2. network request

Code: Select all

msgbox py("exec", statements := "
(
def get_public_ip():
    try:
        from urllib.request import urlopen
        with urlopen('https://api.ipify.org') as response:
            return response.read().decode('utf-8')
    except Exception as e:
        return str(e)
public_ip = get_public_ip()
)", 1)["public_ip"] ; ⟹ (e.g.)  "69.200.103.49"
To set this up:
1. make sure you have python runtime installed (the example code used below uses some syntax only available for python 3.10+)
2. install pywin32 by downloading the correct version from the https://github.com/mhammond/pywin32/releases
3. save the python code further below at the bottom of this post(adapted from @MrDoge's example at https://stackoverflow.com/questions/65780086/how-to-program-hotstrings-in-python-like-in-autohotkey#65783573) as (say) "AHKCOMServer.py"
4. Register the COM server:

Code: Select all

python "AHKCOMServer.py" --register
. The random GUID chosen for the example is "{C70F3BF7-2947-4F87-B31E-9F5B8B13D24F}". The effect of registering the COM server can be found in the registry at HKEY_CLASSES_ROOT\CLSID\{C70F3BF7-2947-4F87-B31E-9F5B8B13D24F}
5. include this function in your AHK code (replace "JSON.parse" with the name of your json deserialization function)

Code: Select all

py(action, param?, jsonParseResult:=0){
	static comServer := ComObject("Python.AHKCOMServer")
	result := comServer.COMServerHandler(action, param?)
	if jsonParseResult
		result := JSON.parse(result)
	return result
}
6. Run the above examples.


Further dicussions/tutorials:
viewtopic.php?t=29394
https://stackoverflow.com/questions/58599829/call-python-function-with-arguments-and-get-returned-value-in-autohotkey/67428298#67428298

AHKCOMServer.py:

Code: Select all

import uuid
import base64
import os
import datetime
import json
import types

class BasicServer:
    _public_methods_ = ["COMServerHandler"]

    @staticmethod
    def COMServerHandler(action, param=None):
        """Handles different operations based on the action."""
        match action:
            case "uuid_2_shortid":
                return BasicServer.dotnet_uuid2shortid(param)
            case "shortid_2_uuid":
                return BasicServer.dotnet_shortid2uuid(param)
            case "ticks_to_readable_time":
                return BasicServer.ticks_to_readable_time(param)
            case "time_now_in_ticks":
                return BasicServer.time_now_in_ticks()
            case "eval":
                return BasicServer.eval(param)
            case "exec":
                return BasicServer.exec(param)
            case _:
                return 0

    @staticmethod
    def dotnet_uuid2shortid(longID):
        """Converts a GUID to a shorter ID representation."""
        b64 = (
            base64.urlsafe_b64encode(uuid.UUID(longID).bytes_le)
            .rstrip(b"=")
            .decode("utf-8")
        )
        shortID = b64.replace("+", "-").replace("/", "_")
        return shortID[:22]

    @staticmethod
    def dotnet_shortid2uuid(shortID):
        """Converts a shorter ID back to a GUID."""
        b64 = shortID.replace("-", "+").replace("_", "/") + "=="
        longID = str(uuid.UUID(bytes_le=base64.urlsafe_b64decode(b64)))
        return longID

    @staticmethod
    def ticks_to_readable_time(ticks):
        """Converts .NET ticks to a human-readable datetime."""
        return str(
            datetime.datetime(1, 1, 1)
            + datetime.timedelta(microseconds=int(ticks) // 10)
        )

    @staticmethod
    def time_now_in_ticks():
        """Returns the current time in .NET ticks."""
        dotnet_epoch = datetime.datetime(1, 1, 1)
        current_utc_time = datetime.datetime.utcnow()
        time_span = current_utc_time - dotnet_epoch
        ticks = time_span.total_seconds() * 10000000
        return int(ticks)

    @staticmethod
    def eval(code_str):
        try:
            # Evaluate the string as Python code and return the result.
            return eval(code_str)
        except Exception as e:
            # Catch and return any errors that occur during evaluation.
            return str(e)

    @staticmethod
    def exec(statements):
        try:
            # Create a dictionary to capture local variables after exec
            local_variables = {}
            global_variables = {}
            exec(statements, global_variables, local_variables)
            serializable_locals = {k: v for k, v in local_variables.items() if not isinstance(v, (types.ModuleType, types.FunctionType))}
            return json.dumps(serializable_locals)
        except Exception as e:
            # Catch and return any errors that occur during exec
            return str(e)


if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print("Error: need to supply arg (" "--register" " or " "--unregister" ")")
        sys.exit(1)
    else:
        import win32com.server.register
        import win32com.server.exception

        myClsid = "{C70F3BF7-2947-4F87-B31E-9F5B8B13D24F}"
        myProgID = "Python.AHKCOMServer"
        if sys.argv[1] == "--register":
            import pythoncom
            import os.path

            realPath = os.path.realpath(__file__)
            nameNoExt = os.path.splitext(os.path.basename(realPath))[0]

            """
               https://github.com/mhammond/pywin32/blob/main/com/win32com/server/register.py
               Registers a Python object as a COM Server.  This enters almost all necessary
               information in the system registry, allowing COM to use the object.

               clsid -- The (unique) CLSID of the server.
               pythonInstString -- A string holding the instance name that will be created
                             whenever COM requests a new object.
               desc -- The description of the COM object.
               progID -- The user name of this object (eg, Word.Document)
               verProgId -- The user name of this version's implementation (eg Word.6.Document)
               defIcon -- The default icon for the object.
               threadingModel -- The threading model this object supports.
               policy -- The policy to use when creating this object.
               catids -- A list of category ID's this object belongs in.
               other -- A dictionary of extra items to be registered.
               addPyComCat -- A flag indicating if the object should be added to the list
                        of Python servers installed on the machine.  If None (the default)
                        then it will be registered when running from python source, but
                        not registered if running in a frozen environment.
               dispatcher -- The dispatcher to use when creating this object.
               clsctx -- One of the CLSCTX_* constants.
               addnPath -- An additional path the COM framework will add to sys.path
                           before attempting to create the object.
            """
            win32com.server.register.RegisterServer(
                clsid=myClsid,
                pythonInstString=nameNoExt + ".BasicServer",
                progID=myProgID,
                desc="Basic Server for AHK COM interop",
                clsctx=pythoncom.CLSCTX_LOCAL_SERVER,
                addnPath=os.path.dirname(realPath),
            )
            print("Registered COM server.")
        elif sys.argv[1] == "--unregister":
            print("Starting to unregister...")
            win32com.server.register.UnregisterServer(myClsid, myProgID)
            print("Unregistered COM server.")
        else:
            print("Error: arg not recognized")

Return to “Scripts and Functions (v2)”