diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 0000000..16680f1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Change Log + +## v1.26.0 + +--- +1. Added release version checks for API's +2. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 6458910..a953f9f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,5 @@ include README.md LICENSE include smsdk/config/* recursive-include tests * +include requirements.txt +include CHANGELOG.md diff --git a/mypy.ini b/mypy.ini index a41a744..96fbf8b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -50,4 +50,4 @@ follow_imports = silent # Files free from Static type check errors -files = smsdk/utils.py, smsdk/tool_register.py, smsdk/ma_session.py \ No newline at end of file +files = smsdk/_version.py, smsdk/utils.py, smsdk/tool_register.py, smsdk/ma_session.py diff --git a/setup.py b/setup.py index 228d8e6..3441a0d 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ from setuptools import setup, find_packages +from smsdk._version import version with open("README.md", "r") as fh: long_description = fh.read() @@ -12,7 +13,7 @@ setup( name="smsdk", - version="1.0.26", + version=version, packages=find_packages(exclude=["test*"]), include_package_data=True, install_requires=install_requires, diff --git a/smsdk/_version.py b/smsdk/_version.py new file mode 100644 index 0000000..ea34a1d --- /dev/null +++ b/smsdk/_version.py @@ -0,0 +1,122 @@ +# +""" +Track version information for sightmachine-sdk module here. +""" +import datetime +import typing as t_ +import itertools +import requests +import warnings +import functools +import re + + +class VersionInfo(t_.NamedTuple): + major: int + minor: int + patchlevel: int + releaselevel: t_.Optional[str] + serial: t_.Optional[int] + + def __str__(self) -> str: + result = "".join( + itertools.chain( + ".".join(str(i) for i in self[:3] if i is not None), + (str(i) for i in self[3:] if i is not None), + ) + ) + return result + + +version_info = VersionInfo(1, 26, 0, "", None) +version = str(f"v{version_info}") + + +def sort_releases_descending( + releases: t_.List[t_.Dict[str, t_.Any]] +) -> t_.List[t_.Dict[str, t_.Any]]: + def version_key(release: t_.Dict[str, t_.Any]) -> t_.List[t_.Union[int, str]]: + return [ + (int(i) if i.isdigit() else i) + for i in re.split(r"(\d+|\W+)", release["tag_name"].lower()) + if i + ] + + return ( + sorted(releases, key=version_key, reverse=True) + if len(releases) > 1 + else releases + ) + + +def get_latest_release_version( + releases: t_.List[t_.Dict[str, t_.Any]] +) -> t_.Optional[t_.Any]: + if releases: + sorted_releases = sort_releases_descending(releases) + return sorted_releases[0]["tag_name"] + return None + + +def get_latest_sdk_release() -> t_.Optional[t_.Any]: + api_url = f"https://api.github.com/repos/{owner}/{repo}/releases" + + try: + response = requests.get(api_url, timeout=10) + response.raise_for_status() # To raise an HTTPError for any bad responses + releases = response.json() + + if releases: + return get_latest_release_version(releases) + except requests.RequestException as e: + print(f"Error fetching latest SDK release: {e}") + + return None + + +class VersionCheckDecorator: + api_version_printed = False + last_version_check_time = None + + @classmethod + def version_check_decorator(cls, func: t_.Any) -> t_.Any: + @functools.wraps(func) + def wrapper(*args: t_.Any, **kwargs: t_.Any) -> t_.Any: + current_time = datetime.datetime.now() + + # Check if a week has passed since the last version check + if cls.last_version_check_time is None or ( + current_time - cls.last_version_check_time > datetime.timedelta(days=7) + ): + cls.api_version_printed = False + cls.last_version_check_time = current_time + + if not cls.api_version_printed: + latest_sdk_release = get_latest_sdk_release() + installed_sdk_release = version + + if ( + installed_sdk_release is not None + and latest_sdk_release is not None + and latest_sdk_release != installed_sdk_release + ): + warnings.warn( + f"Installed SDK Version: {installed_sdk_release}. " + f"It is recommended to install release version ({latest_sdk_release}).", + DeprecationWarning, + ) + cls.api_version_printed = True + + return func(*args, **kwargs) + + return wrapper + + +# Initialize the flag to False +api_version_printed = False + +owner = "sightmachine" +repo = "sightmachine-sdk" + +# Define version_check_decorator here +version_check_decorator = VersionCheckDecorator.version_check_decorator diff --git a/smsdk/client.py b/smsdk/client.py index 296b380..270c461 100644 --- a/smsdk/client.py +++ b/smsdk/client.py @@ -2,6 +2,7 @@ # coding: utf-8 """ Sight Machine SDK Client """ from __future__ import unicode_literals, absolute_import +from smsdk._version import version_check_decorator import pandas as pd import numpy as np @@ -151,17 +152,20 @@ def __init__(self, tenant, site_domain="sightmachine.io", protocol="https"): self.auth = Authenticator(self) self.session = self.auth.session + @version_check_decorator def select_db_schema(self, schema_name): # remove X_SM_WRKSPACE_ID from self.session.headers self.session.headers.update({X_SM_DB_SCHEMA: schema_name}) if X_SM_WORKSPACE_ID in self.session.headers: del self.session.headers[X_SM_WORKSPACE_ID] + @version_check_decorator def select_workspace_id(self, workspace_id): self.session.headers.update({X_SM_WORKSPACE_ID: str(workspace_id)}) if X_SM_DB_SCHEMA in self.session.headers: del self.session.headers[X_SM_DB_SCHEMA] + @version_check_decorator def get_data_v1(self, ename, util_name, normalize=True, *args, **kwargs): """ Main data fetching function for all the entities. Note this is the general data fetch function. You probably want to use the model-specific functions such as get_cycles(). @@ -234,6 +238,7 @@ def get_data_v1(self, ename, util_name, normalize=True, *args, **kwargs): return data + @version_check_decorator @ClientV0.validate_input @ClientV0.cycle_decorator def get_cycles( @@ -248,6 +253,7 @@ def get_cycles( return df + @version_check_decorator @ClientV0.validate_input @ClientV0.downtime_decorator def get_downtimes( @@ -262,6 +268,7 @@ def get_downtimes( return df + @version_check_decorator @ClientV0.validate_input @ClientV0.part_decorator def get_parts( @@ -277,6 +284,7 @@ def get_parts( return df + @version_check_decorator def get_kpis(self, **kwargs): kpis = smsdkentities.get("kpi") base_url = get_url( @@ -284,6 +292,7 @@ def get_kpis(self, **kwargs): ) return kpis(self.session, base_url).get_kpis(**kwargs) + @version_check_decorator def get_machine_type_from_clean_name(self, kwargs): # Get machine_types dataframe to check display name machine_types_df = self.get_machine_types() @@ -304,6 +313,7 @@ def get_machine_type_from_clean_name(self, kwargs): return machine_types + @version_check_decorator def get_machine_source_from_clean_name(self, kwargs): # Get machines dataframe to check display/clean name machine_sources_df = self.get_machines() @@ -324,6 +334,7 @@ def get_machine_source_from_clean_name(self, kwargs): return machine_sources + @version_check_decorator def get_kpis_for_asset(self, **kwargs): kpis = smsdkentities.get("kpi") base_url = get_url( @@ -343,6 +354,7 @@ def get_kpis_for_asset(self, **kwargs): return kpis(self.session, base_url).get_kpis_for_asset(**kwargs) + @version_check_decorator def get_kpi_data_viz( self, machine_sources=None, @@ -390,6 +402,7 @@ def get_kpi_data_viz( ] = self.get_machine_source_from_clean_name(kwargs) return kpi_entity(self.session, base_url).get_kpi_data_viz(**kwargs) + @version_check_decorator def get_type_from_machine(self, machine_source=None, **kwargs): machine = smsdkentities.get("machine") base_url = get_url( @@ -399,6 +412,7 @@ def get_type_from_machine(self, machine_source=None, **kwargs): machine_source, **kwargs ) + @version_check_decorator def get_machine_schema( self, machine_source=None, @@ -436,6 +450,7 @@ def get_machine_schema( return frame + @version_check_decorator def get_fields_of_machine_type( self, machine_type=None, @@ -460,6 +475,7 @@ def get_fields_of_machine_type( return fields + @version_check_decorator def get_cookbooks(self, **kwargs): """ Gets all of the cookbooks accessable to the logged in user. @@ -471,6 +487,7 @@ def get_cookbooks(self, **kwargs): ) return cookbook(self.session, base_url).get_cookbooks(**kwargs) + @version_check_decorator def get_cookbook_top_results(self, recipe_group_id=None, limit=10, **kwargs): """ Gets the top runs for a recipe group. @@ -486,6 +503,7 @@ def get_cookbook_top_results(self, recipe_group_id=None, limit=10, **kwargs): recipe_group_id, limit, **kwargs ) + @version_check_decorator def get_cookbook_current_value(self, variables=[], minutes=1440, **kwargs): """ Gets the current value of a field. @@ -501,6 +519,7 @@ def get_cookbook_current_value(self, variables=[], minutes=1440, **kwargs): variables, minutes, **kwargs ) + @version_check_decorator def normalize_constraint(self, constraint): """ Takes a constraint and returns a string version of it's to and from fields. @@ -513,6 +532,7 @@ def normalize_constraint(self, constraint): from_symbol = "]" if constraint.get("from_is_inclusive") else ")" return "{}{},{}{}".format(to_symbol, to_val, from_val, from_symbol) + @version_check_decorator def normalize_constraints(self, constraints): """ Takes a list of constraint and returns string versions of their to and from fields. @@ -524,6 +544,7 @@ def normalize_constraints(self, constraints): constraints_normal.append(self.normalize_constraint(constraint)) return constraints_normal + @version_check_decorator def get_lines(self, **kwargs): """ Returns all the lines for the facility @@ -534,6 +555,7 @@ def get_lines(self, **kwargs): ) return lines(self.session, base_url).get_lines(**kwargs) + @version_check_decorator def get_line_data( self, assets=None, @@ -583,6 +605,7 @@ def get_line_data( limit=limit, offset=offset, **kwargs ) + @version_check_decorator def create_share_link( self, assets=None, @@ -626,6 +649,7 @@ def create_share_link( *args, assets, chartType, yAxis, xAxis, model, time_selection, **kwargs ) + @version_check_decorator def get_machines(self, normalize=True, *args, **kwargs): """ Get list of machines and associated metadata. Note this includes extensive internal metadata. If you only want to get a list of machine names @@ -639,6 +663,7 @@ def get_machines(self, normalize=True, *args, **kwargs): "machine_v1", "get_machines", normalize, *args, **kwargs ) + @version_check_decorator def get_machine_names(self, source_type=None, clean_strings_out=True): """ Get a list of machine names. This is a simplified version of get_machines(). @@ -676,6 +701,7 @@ def get_machine_names(self, source_type=None, clean_strings_out=True): else: return machines["source"].to_list() + @version_check_decorator def get_machine_types(self, source_type=None, *args, **kwargs): """ Get list of machine types and associated metadata. Note this includes extensive internal metadata. If you only want to get a list of machine type names @@ -695,6 +721,7 @@ def get_machine_types(self, source_type=None, *args, **kwargs): return mts + @version_check_decorator def get_machine_type_names(self, clean_strings_out=True): """ Get a list of machine type names. This is a simplified version of get_machine_types(). @@ -715,6 +742,7 @@ def get_machine_type_names(self, clean_strings_out=True): else: return machine_types["source_type"].unique().tolist() + @version_check_decorator def get_raw_data( self, raw_data_table=None,