From ebbc0c00ae2cfd4bfc511d2a1db230b57929ba18 Mon Sep 17 00:00:00 2001 From: Thomas Edward Kingstone Date: Mon, 19 Jan 2026 13:01:02 +0000 Subject: [PATCH 1/7] added log reading methods, and also minor improvements to error capture --- .../src/python_toolkit/bhom/__init__.py | 8 ++ .../src/python_toolkit/bhom/analytics.py | 118 ++++++++++++++++-- 2 files changed, 117 insertions(+), 9 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/__init__.py b/Python_Engine/Python/src/python_toolkit/bhom/__init__.py index 28f3edc..bc0efd2 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/__init__.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/__init__.py @@ -1,5 +1,6 @@ """Root for the bhom subpackage.""" +import os from pathlib import Path # pylint: disable=E0401 from os import path import tempfile @@ -9,6 +10,13 @@ TOOLKIT_NAME = "Python_Toolkit" BHOM_VERSION = importlib.metadata.version("python_toolkit") +#Environment variable that if set disables BHoM analytics logging. +DISABLE_ANALYTICS = os.environ.get("DISABLE_BHOM_ANALYTICS", None) +if DISABLE_ANALYTICS is None: + DISABLE_ANALYTICS = False +else: + DISABLE_ANALYTICS = True + if not BHOM_LOG_FOLDER.exists(): BHOM_LOG_FOLDER = Path(tempfile.gettempdir()) / "BHoM" / "Logs" BHOM_LOG_FOLDER.mkdir(exist_ok=True, parents=True) \ No newline at end of file diff --git a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py index 0dcbf88..e612b04 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py @@ -1,21 +1,118 @@ """BHoM analytics decorator.""" # pylint: disable=E0401 +from _typeshed import OptExcInfo +import codecs +from dataclasses import dataclass, field import inspect +from itertools import groupby +import itertools import json +import os +from pathlib import Path +import socket import sys +import traceback import uuid from functools import wraps -from typing import Any, Callable +from typing import Any, Callable, Dict, List from datetime import datetime # pylint: enable=E0401 from .logging import ANALYTICS_LOGGER from .util import csharp_ticks -from . import BHOM_VERSION, TOOLKIT_NAME, BHOM_LOG_FOLDER - - -def bhom_analytics() -> Callable: +from . import BHOM_VERSION, TOOLKIT_NAME, BHOM_LOG_FOLDER, DISABLE_ANALYTICS + +@dataclass +class UsageLogEntry(): + BHoMVersion:str = BHOM_VERSION + BHoM_Guid:uuid.UUID = uuid.uuid4() + CallerName:str = "" + ComponentId:uuid.UUID = uuid.uuid4() + CustomData:Dict = field(default_factory = {"interpreter", sys.executable}) + Errors:List[str] = field(default_factory = []) + FileId:str = "" + FileName:str = "" + Fragments:List[str] = field(default_factory = []) + Name:str = "" + ProjectID:str = "" + SelectedItem:Dict = field(default_factory = {"MethodName": "", "Parameters": [], "TypeName": ""}) + Time:Dict = field(default_factory = {"$date": 0}) + UI:str = "Python" + UiVersion:str = TOOLKIT_NAME + _t:str = "BH.oM.Base.UsageLogEntry" + + @classmethod + def from_json(cls, json_str:str) -> 'UsageLogEntry': + d = json.loads(json_str) + if "CustomData" not in d: + d["CustomData"] = None + if "Fragments" not in d: + d["Fragments"] = None + return UsageLogEntry(d["BHoMVersion"], d["BHoM_Guid"], d["CallerName"], d["ComponentId"], d["CustomData"], d["Errors"], d["FileId"], d["FileName"], d["Fragments"], d["Name"], d["ProjectID"], d["SelectedItem"], d["Time"], d["UI"], d["UiVersion"]) + +def load_logs_from_file(filename:str, delete_processed_files: bool = False) -> List[UsageLogEntry]: + logs:List[UsageLogEntry] = [] + + #adapted from https://stackoverflow.com/questions/30629297/remove-byte-order-mark-from-objects-in-a-list + #due to some files generated by BHoM logs being encoded with utf-8 BOM instead of utf-8 + with open(filename, "r") as f: + lines = f.readlines() + if lines[0].__contains__(codecs.BOM_UTF8.decode(f.encoding)): + # A Byte Order Mark is present + lines[0] = lines[0].strip(codecs.BOM_UTF8.decode(f.encoding)) + for line in lines: + if len(line) != 0: + logs.append(UsageLogEntry.from_json(line)) + + if delete_processed_files: + Path(filename).unlink() + + return logs + +def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: + db_entries:List[Dict] = [] + + usage_log_entries.sort(key=lambda x: x.ProjectID) + + for project_id, projectgroup in groupby(usage_log_entries, lambda x: x.ProjectID): + projectgroup = list(projectgroup) + projectgroup.sort(key = lambda x: x.CallerName + str(x.SelectedItem)) + + for method_name, methodgroup in groupby(projectgroup, lambda x: x.CallerName + str(x.SelectedItem)): + methodgroup = list(methodgroup) + first_entry = methodgroup[0] + + db_entries.append({ + "StartTime": min(methodgroup, key=lambda x: x.Time["$date"]).Time, + "EndTime": min(methodgroup, key=lambda x: x.Time["$date"]).Time, + "UI": first_entry.UI, + "UiVersion":first_entry.UiVersion, + "CallerName": first_entry.CallerName, + "SelectedItem": first_entry.SelectedItem, + "Computer": socket.gethostname(), #no clue why machine name is a socker function rather than os... + "UserName": os.environ.get("USERNAME"), + "BHoMVersion": BHOM_VERSION, + "FileId": "", + "FileName": "", + "ProjectID": project_id, + "NbCallingComponents": len(set([a.ComponentId for a in methodgroup])), + "TotalNbCals": len(methodgroup), + "Errors": list(itertools.chain.from_iterable([x.Errors for x in methodgroup])), + "_t": "BH.oM.BHoMAnalytics.UsageEntry" + }) + + return db_entries + +def convert_exc_info_to_bhom_error(exc_info:OptExcInfo): + time = csharp_ticks(datetime.now()) + utcTime = csharp_ticks() + stack_trace = traceback.extract_tb(exc_info[3]) + message = str(exc_info[1]) + Type = "Error" #using string but ideally this would be an enum value. + return {"Time": {"$date": time}, "UtcTime": {"$date": utcTime}, "StackTrace": stack_trace, "Message": message, "Type": Type, "_t": "BH.oM.Base.Debugging.Event"} + +def bhom_analytics(project_id:str = "", disable:bool = DISABLE_ANALYTICS) -> Callable: """Decorator for capturing usage data. Returns @@ -41,7 +138,10 @@ def decorator(function: Callable): @wraps(function) def wrapper(*args, **kwargs) -> Any: """A wrapper around the function that captures usage analytics.""" - + + if disable: + return function(*args, **kwargs) + _id = uuid.uuid4() # get the data being passed to the function, expected dtype and return type @@ -63,7 +163,7 @@ def wrapper(*args, **kwargs) -> Any: "Name": "", # TODO - get project properties from another function/logging # method (or from BHoM DLL analytics capture ...) - "ProjectID": "", + "ProjectID": project_id, "SelectedItem": { "MethodName": function.__name__, "Parameters": _args, @@ -80,10 +180,10 @@ def wrapper(*args, **kwargs) -> Any: try: result = function(*args, **kwargs) except Exception as exc: # pylint: disable=broad-except - exec_metadata["Errors"].extend(sys.exc_info()) + exec_metadata["Errors"].extend(convert_exc_info_to_bhom_error(sys.exc_info())) raise exc finally: - log_file = BHOM_LOG_FOLDER / f"{function.__module__.split('.')[0]}_{datetime.now().strftime('%Y%m%d')}.log" + log_file = BHOM_LOG_FOLDER / f"Usage_{function.__module__.split('.')[0]}_{datetime.now().strftime('%Y%m%d')}.log" if ANALYTICS_LOGGER.handlers[0].baseFilename != str(log_file): ANALYTICS_LOGGER.handlers[0].close() From e5f8425748de6f5a58cc62299f654f28358d17a4 Mon Sep 17 00:00:00 2001 From: Thomas Edward Kingstone Date: Tue, 20 Jan 2026 09:24:51 +0000 Subject: [PATCH 2/7] added helper methods for analytics --- .../src/python_toolkit/bhom/analytics.py | 19 ++++++++++--------- .../Python/src/python_toolkit/bhom/util.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py index e612b04..5be1c7e 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py @@ -1,6 +1,5 @@ """BHoM analytics decorator.""" # pylint: disable=E0401 -from _typeshed import OptExcInfo import codecs from dataclasses import dataclass, field import inspect @@ -20,7 +19,7 @@ # pylint: enable=E0401 from .logging import ANALYTICS_LOGGER -from .util import csharp_ticks +from .util import csharp_ticks, ticks_to_datetime from . import BHOM_VERSION, TOOLKIT_NAME, BHOM_LOG_FOLDER, DISABLE_ANALYTICS @dataclass @@ -83,9 +82,11 @@ def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: methodgroup = list(methodgroup) first_entry = methodgroup[0] + errors = list(itertools.chain.from_iterable([x.Errors for x in methodgroup])) + db_entries.append({ - "StartTime": min(methodgroup, key=lambda x: x.Time["$date"]).Time, - "EndTime": min(methodgroup, key=lambda x: x.Time["$date"]).Time, + "StartTime": ticks_to_datetime(min(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), + "EndTime": ticks_to_datetime(min(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), "UI": first_entry.UI, "UiVersion":first_entry.UiVersion, "CallerName": first_entry.CallerName, @@ -104,15 +105,15 @@ def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: return db_entries -def convert_exc_info_to_bhom_error(exc_info:OptExcInfo): - time = csharp_ticks(datetime.now()) - utcTime = csharp_ticks() +def convert_exc_info_to_bhom_error(exc_info): + time = csharp_ticks(datetime.now(), short=True) + utcTime = csharp_ticks(short=True) stack_trace = traceback.extract_tb(exc_info[3]) message = str(exc_info[1]) Type = "Error" #using string but ideally this would be an enum value. return {"Time": {"$date": time}, "UtcTime": {"$date": utcTime}, "StackTrace": stack_trace, "Message": message, "Type": Type, "_t": "BH.oM.Base.Debugging.Event"} -def bhom_analytics(project_id:str = "", disable:bool = DISABLE_ANALYTICS) -> Callable: +def bhom_analytics(project_id:Callable = lambda: "", disable:bool = DISABLE_ANALYTICS) -> Callable: """Decorator for capturing usage data. Returns @@ -163,7 +164,7 @@ def wrapper(*args, **kwargs) -> Any: "Name": "", # TODO - get project properties from another function/logging # method (or from BHoM DLL analytics capture ...) - "ProjectID": project_id, + "ProjectID": project_id(), "SelectedItem": { "MethodName": function.__name__, "Parameters": _args, diff --git a/Python_Engine/Python/src/python_toolkit/bhom/util.py b/Python_Engine/Python/src/python_toolkit/bhom/util.py index 3200384..22b1287 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/util.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/util.py @@ -1,7 +1,6 @@ """General utility functions.""" # pylint: disable=E0401 -from datetime import datetime - +from datetime import datetime, timedelta # pylint: enable=E0401 @@ -22,3 +21,10 @@ def csharp_ticks(date_time: datetime = datetime.utcnow(), short: bool = False) - return int(_ticks) return int(_ticks * (10**7)) + +def ticks_to_datetime(ticks: int, short:bool = False) -> datetime: + + if not short: + _ticks *= (10**-7) + + return datetime(1, 1, 1) + timedelta(seconds=ticks) \ No newline at end of file From cbb40eaac86473eb168843547dc68dd758f58ac8 Mon Sep 17 00:00:00 2001 From: Thomas Edward Kingstone Date: Tue, 20 Jan 2026 11:11:04 +0000 Subject: [PATCH 3/7] change console logger default level, and some fixes for analytics, create global for setting/getting a persistent project number --- .../src/python_toolkit/bhom/analytics.py | 23 +++++++++++++++---- .../python_toolkit/bhom/logging/console.py | 6 +++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py index 5be1c7e..a651bd9 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py @@ -13,12 +13,12 @@ import traceback import uuid from functools import wraps -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Union from datetime import datetime # pylint: enable=E0401 -from .logging import ANALYTICS_LOGGER +from .logging import ANALYTICS_LOGGER, CONSOLE_LOGGER from .util import csharp_ticks, ticks_to_datetime from . import BHOM_VERSION, TOOLKIT_NAME, BHOM_LOG_FOLDER, DISABLE_ANALYTICS @@ -113,7 +113,19 @@ def convert_exc_info_to_bhom_error(exc_info): Type = "Error" #using string but ideally this would be an enum value. return {"Time": {"$date": time}, "UtcTime": {"$date": utcTime}, "StackTrace": stack_trace, "Message": message, "Type": Type, "_t": "BH.oM.Base.Debugging.Event"} -def bhom_analytics(project_id:Callable = lambda: "", disable:bool = DISABLE_ANALYTICS) -> Callable: +global PROJECT_NUMBER +PROJECT_NUMBER = None + +def set_project_number(project_number: Union[str, None]): + global PROJECT_NUMBER + CONSOLE_LOGGER.debug(f"Setting project number: {PROJECT_NUMBER} to {project_number}") + PROJECT_NUMBER = project_number + +def get_project_number() -> Union[str, None]: + CONSOLE_LOGGER.debug(f"Retrieving project number: {PROJECT_NUMBER}") + return PROJECT_NUMBER + +def bhom_analytics(project_id:Callable = get_project_number, disable:bool = DISABLE_ANALYTICS) -> Callable: """Decorator for capturing usage data. Returns @@ -139,8 +151,9 @@ def decorator(function: Callable): @wraps(function) def wrapper(*args, **kwargs) -> Any: """A wrapper around the function that captures usage analytics.""" - + if disable: + CONSOLE_LOGGER.debug("bhom_analytics is curently disabled.") return function(*args, **kwargs) _id = uuid.uuid4() @@ -156,7 +169,7 @@ def wrapper(*args, **kwargs) -> Any: "BHoM_Guid": _id, "CallerName": function.__name__, "ComponentId": _id, - "CustomData": {"interpreter", sys.executable}, + "CustomData": {"interpreter": sys.executable}, "Errors": [], "FileId": "", "FileName": "", diff --git a/Python_Engine/Python/src/python_toolkit/bhom/logging/console.py b/Python_Engine/Python/src/python_toolkit/bhom/logging/console.py index d36178b..029dc9a 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/logging/console.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/logging/console.py @@ -8,12 +8,14 @@ from .. import TOOLKIT_NAME +level = logging.INFO + formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s") handler = logging.StreamHandler(sys.stdout) -handler.setLevel(logging.DEBUG) +handler.setLevel(level) handler.setFormatter(formatter) CONSOLE_LOGGER = logging.getLogger(f"{TOOLKIT_NAME}[console]") CONSOLE_LOGGER.propagate = False -CONSOLE_LOGGER.setLevel(logging.DEBUG) +CONSOLE_LOGGER.setLevel(level) CONSOLE_LOGGER.addHandler(handler) From c3e02236a24685c4d3d8ff60efa0227c1e7d7347 Mon Sep 17 00:00:00 2001 From: Thomas Edward Kingstone Date: Tue, 20 Jan 2026 13:35:22 +0000 Subject: [PATCH 4/7] Apply suggestions from code review --- Python_Engine/Python/src/python_toolkit/bhom/analytics.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py index a651bd9..b58fea7 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py @@ -38,7 +38,7 @@ class UsageLogEntry(): SelectedItem:Dict = field(default_factory = {"MethodName": "", "Parameters": [], "TypeName": ""}) Time:Dict = field(default_factory = {"$date": 0}) UI:str = "Python" - UiVersion:str = TOOLKIT_NAME + UiVersion:str = sys.version _t:str = "BH.oM.Base.UsageLogEntry" @classmethod @@ -86,12 +86,12 @@ def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: db_entries.append({ "StartTime": ticks_to_datetime(min(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), - "EndTime": ticks_to_datetime(min(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), + "EndTime": ticks_to_datetime(max(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), "UI": first_entry.UI, "UiVersion":first_entry.UiVersion, "CallerName": first_entry.CallerName, "SelectedItem": first_entry.SelectedItem, - "Computer": socket.gethostname(), #no clue why machine name is a socker function rather than os... + "Computer": socket.gethostname(), "UserName": os.environ.get("USERNAME"), "BHoMVersion": BHOM_VERSION, "FileId": "", @@ -175,8 +175,6 @@ def wrapper(*args, **kwargs) -> Any: "FileName": "", "Fragments": [], "Name": "", - # TODO - get project properties from another function/logging - # method (or from BHoM DLL analytics capture ...) "ProjectID": project_id(), "SelectedItem": { "MethodName": function.__name__, From 7571b87839ebe7afe1fe711f1eec80ad4a7e3179 Mon Sep 17 00:00:00 2001 From: Thomas Edward Kingstone Date: Thu, 22 Jan 2026 14:28:21 +0000 Subject: [PATCH 5/7] component id to allow more precise logging. --- .../Python/src/python_toolkit/bhom/analytics.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py index b58fea7..0146571 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py @@ -50,7 +50,7 @@ def from_json(cls, json_str:str) -> 'UsageLogEntry': d["Fragments"] = None return UsageLogEntry(d["BHoMVersion"], d["BHoM_Guid"], d["CallerName"], d["ComponentId"], d["CustomData"], d["Errors"], d["FileId"], d["FileName"], d["Fragments"], d["Name"], d["ProjectID"], d["SelectedItem"], d["Time"], d["UI"], d["UiVersion"]) -def load_logs_from_file(filename:str, delete_processed_files: bool = False) -> List[UsageLogEntry]: +def load_logs_from_file(filename:str) -> List[UsageLogEntry]: logs:List[UsageLogEntry] = [] #adapted from https://stackoverflow.com/questions/30629297/remove-byte-order-mark-from-objects-in-a-list @@ -64,9 +64,6 @@ def load_logs_from_file(filename:str, delete_processed_files: bool = False) -> L if len(line) != 0: logs.append(UsageLogEntry.from_json(line)) - if delete_processed_files: - Path(filename).unlink() - return logs def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: @@ -82,8 +79,6 @@ def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: methodgroup = list(methodgroup) first_entry = methodgroup[0] - errors = list(itertools.chain.from_iterable([x.Errors for x in methodgroup])) - db_entries.append({ "StartTime": ticks_to_datetime(min(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), "EndTime": ticks_to_datetime(max(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), @@ -134,6 +129,8 @@ def bhom_analytics(project_id:Callable = get_project_number, disable:bool = DISA The decorated function. """ + _componentId = uuid.uuid4() + def decorator(function: Callable): """A decorator to capture usage data for called methods/functions. @@ -168,7 +165,7 @@ def wrapper(*args, **kwargs) -> Any: "BHoMVersion": BHOM_VERSION, "BHoM_Guid": _id, "CallerName": function.__name__, - "ComponentId": _id, + "ComponentId": _componentId, "CustomData": {"interpreter": sys.executable}, "Errors": [], "FileId": "", @@ -185,7 +182,7 @@ def wrapper(*args, **kwargs) -> Any: "$date": csharp_ticks(short=True), }, "UI": "Python", - "UiVersion": TOOLKIT_NAME, + "UiVersion": sys.version, "_t": "BH.oM.Base.UsageLogEntry", } From e60c0ef29bfe4cbf3b8bb71994ee13b3cab9a93f Mon Sep 17 00:00:00 2001 From: Thomas Edward Kingstone Date: Mon, 26 Jan 2026 12:54:46 +0000 Subject: [PATCH 6/7] make summarise more like BHoM analytics and improve analytics logger to add (currently dummy) file IDs --- .../src/python_toolkit/bhom/analytics.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py index 0146571..c6e0721 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py @@ -71,11 +71,13 @@ def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: usage_log_entries.sort(key=lambda x: x.ProjectID) - for project_id, projectgroup in groupby(usage_log_entries, lambda x: x.ProjectID): - projectgroup = list(projectgroup) - projectgroup.sort(key = lambda x: x.CallerName + str(x.SelectedItem)) + for file_id, filegroup in groupby(usage_log_entries, lambda x: x.FileId): + filegroup = list(filegroup) + project_id = filegroup[0].ProjectID + filename = filegroup[0].FileName + filegroup.sort(key = lambda x: x.CallerName + str(x.SelectedItem)) - for method_name, methodgroup in groupby(projectgroup, lambda x: x.CallerName + str(x.SelectedItem)): + for method_name, methodgroup in groupby(filegroup, lambda x: x.CallerName + str(x.SelectedItem)): methodgroup = list(methodgroup) first_entry = methodgroup[0] @@ -89,8 +91,8 @@ def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: "Computer": socket.gethostname(), "UserName": os.environ.get("USERNAME"), "BHoMVersion": BHOM_VERSION, - "FileId": "", - "FileName": "", + "FileId": file_id, + "FileName": filename, "ProjectID": project_id, "NbCallingComponents": len(set([a.ComponentId for a in methodgroup])), "TotalNbCals": len(methodgroup), @@ -155,6 +157,9 @@ def wrapper(*args, **kwargs) -> Any: _id = uuid.uuid4() + #for now for file IDs, generate one using the project ID + file_id = uuid.uuid3(uuid.NAMESPACE_OID, project_id()) + # get the data being passed to the function, expected dtype and return type argspec = inspect.getfullargspec(function)[-1] argspec.pop("return", None) @@ -168,8 +173,8 @@ def wrapper(*args, **kwargs) -> Any: "ComponentId": _componentId, "CustomData": {"interpreter": sys.executable}, "Errors": [], - "FileId": "", - "FileName": "", + "FileId": str(file_id), + "FileName": str(file_id), "Fragments": [], "Name": "", "ProjectID": project_id(), From 31f1a152933d960df904a9c32b5ac57faea24cda Mon Sep 17 00:00:00 2001 From: Felix Mallinder Date: Wed, 28 Jan 2026 10:47:34 +0000 Subject: [PATCH 7/7] fix implementation of c# ticks to be unix ticks to better reflect BHoM_Engine serialiser --- .../Python/src/python_toolkit/bhom/analytics.py | 12 ++++++------ .../Python/src/python_toolkit/bhom/util.py | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py index c6e0721..fbffddf 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/analytics.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/analytics.py @@ -19,7 +19,7 @@ # pylint: enable=E0401 from .logging import ANALYTICS_LOGGER, CONSOLE_LOGGER -from .util import csharp_ticks, ticks_to_datetime +from .util import bson_unix_ticks, bson_unix_ticks_to_datetime from . import BHOM_VERSION, TOOLKIT_NAME, BHOM_LOG_FOLDER, DISABLE_ANALYTICS @dataclass @@ -82,8 +82,8 @@ def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: first_entry = methodgroup[0] db_entries.append({ - "StartTime": ticks_to_datetime(min(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), - "EndTime": ticks_to_datetime(max(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), + "StartTime": bson_unix_ticks_to_datetime(min(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), + "EndTime": bson_unix_ticks_to_datetime(max(methodgroup, key=lambda x: x.Time["$date"]).Time["$date"], short=True), "UI": first_entry.UI, "UiVersion":first_entry.UiVersion, "CallerName": first_entry.CallerName, @@ -103,8 +103,8 @@ def summarise_usage_logs(usage_log_entries:List[UsageLogEntry]) -> List[Dict]: return db_entries def convert_exc_info_to_bhom_error(exc_info): - time = csharp_ticks(datetime.now(), short=True) - utcTime = csharp_ticks(short=True) + time = bson_unix_ticks(datetime.now(), short=True) + utcTime = bson_unix_ticks(short=True) stack_trace = traceback.extract_tb(exc_info[3]) message = str(exc_info[1]) Type = "Error" #using string but ideally this would be an enum value. @@ -184,7 +184,7 @@ def wrapper(*args, **kwargs) -> Any: "TypeName": f"{function.__module__}.{function.__qualname__}" }, "Time": { - "$date": csharp_ticks(short=True), + "$date": bson_unix_ticks(short=True), }, "UI": "Python", "UiVersion": sys.version, diff --git a/Python_Engine/Python/src/python_toolkit/bhom/util.py b/Python_Engine/Python/src/python_toolkit/bhom/util.py index 22b1287..f5491f7 100644 --- a/Python_Engine/Python/src/python_toolkit/bhom/util.py +++ b/Python_Engine/Python/src/python_toolkit/bhom/util.py @@ -4,8 +4,8 @@ # pylint: enable=E0401 -def csharp_ticks(date_time: datetime = datetime.utcnow(), short: bool = False) -> int: - """Python implementation of C# DateTime.UtcNow.Ticks. +def bson_unix_ticks(date_time: datetime = datetime.utcnow(), short: bool = False) -> int: + """Python implementation of unix ticks. Args: date_time (datetime, optional): The datetime to convert to ticks. Defaults to datetime.utcnow(). @@ -15,16 +15,16 @@ def csharp_ticks(date_time: datetime = datetime.utcnow(), short: bool = False) - int: The ticks. """ - _ticks = (date_time - datetime(1, 1, 1)).total_seconds() + _ticks = (date_time - datetime(1970, 1, 1)).total_seconds() * 10**3 if short: return int(_ticks) - return int(_ticks * (10**7)) + return int(_ticks * (10**4)) -def ticks_to_datetime(ticks: int, short:bool = False) -> datetime: +def bson_unix_ticks_to_datetime(ticks: int, short:bool = False) -> datetime: if not short: - _ticks *= (10**-7) + _ticks *= (10**-4) - return datetime(1, 1, 1) + timedelta(seconds=ticks) \ No newline at end of file + return datetime(1, 1, 1) + timedelta(milliseconds=ticks) \ No newline at end of file