diff --git a/gnssanalysis/enum_meta_properties.py b/gnssanalysis/enum_meta_properties.py new file mode 100644 index 0000000..84e424a --- /dev/null +++ b/gnssanalysis/enum_meta_properties.py @@ -0,0 +1,16 @@ +class EnumMetaProperties(type): + """ + This metaclass: + - intercepts attempts to set *class* attributes, and rejects them. + - NOTE: In the class or abstract class using this, you should also define an __init__() which raises + an exception, to prevent instantiation. + - defines the class string representation as being *just* the class name, without any fluff. + + Loosely based on carefully reviewed AI generated examples from Microsoft Copilot. + """ + + def __setattr__(cls, name: str, value) -> None: + raise AttributeError(f"Attributes of {cls} act as constants. Do not modify them.") + + def __repr__(cls) -> str: + return f"{cls.__name__}" diff --git a/gnssanalysis/solution_types.py b/gnssanalysis/solution_types.py new file mode 100644 index 0000000..e661e76 --- /dev/null +++ b/gnssanalysis/solution_types.py @@ -0,0 +1,128 @@ +import logging +from gnssanalysis.enum_meta_properties import EnumMetaProperties + +logging.basicConfig(format="%(asctime)s [%(funcName)s] %(levelname)s: %(message)s") + + +# Abstract base class. Leverages above Immutable metaclass to prevent its (effectively) constants, from being modified. +# Note that this doesn't prevent everything. For example, the contents of a list can still be changed. +class SolutionType(metaclass=EnumMetaProperties): + name: str + long_name: str + + def __init__(self): + raise Exception("This is intended to act akin to an enum. Don't instantiate it.") + + +class FIN(SolutionType): + """ + Final products + """ + + name = "FIN" + long_name = "final" + + +class NRT(SolutionType): + """ + Near-Real Time (between ULT and RTS) + """ + + name = "PRD" + long_name = "near-real time" + + +class PRD(SolutionType): + """ + Predicted products + """ + + name = "PRD" + long_name = "predicted" + + +class RAP(SolutionType): + """ + Rapid products + """ + + name = "RAP" + long_name = "rapid" + + +class RTS(SolutionType): + """ + Real-Time streamed products + """ + + name = "RTS" + long_name = "real-time streamed" + + +class SNX(SolutionType): + """ + SINEX Combination product + """ + + name = "SNX" + long_name = "sinex combination" + + +class ULT(SolutionType): + """ + Ultra-rapid products + The only orbit product from IGS which isn't a 1 day span + """ + + name = "ULT" + long_name = "ultra-rapid" + + +class UNK(SolutionType): + """ + Internal representation of an unknown solution type. + """ + + name = "UNK" + long_name = "unknown solution type" + + +class SolutionTypes(metaclass=EnumMetaProperties): + """ + Defines valid solution type identifiers specified for use in the IGS long product filename convention v2: + https://files.igs.org/pub/resource/guidelines/Guidelines_For_Long_Product_Filenames_in_the_IGS_v2.0.pdf + + Also see here for information on session lengths of products pubished by IGS: https://igs.org/products/#about + """ + + def __init__(self): + raise Exception("This is intended to act akin to an enum. Don't instantiate it.") + + FIN = FIN # Final products + NRT = NRT # Near-Real Time (between ULT and RTS) + PRD = PRD # Predicted products + RAP = RAP # Rapid products + RTS = RTS # Real-Time streamed products + SNX = SNX # SINEX Combination product + ULT = ULT # Ultra-rapid products (every 6 hours). The only orbit product from IGS which isn't a 1 day span + UNK = UNK # Internal representation of unknown. Useful in contexts where defaults are passed as strings. + + # To support search function below + _all: list[type[SolutionType]] = [FIN, NRT, PRD, RAP, RTS, SNX, ULT, UNK] + + @staticmethod + def from_name(name: str): + """ + Returns the relevant static SolutionType object, given the solution type's short name (case insensitive). + :param str name: The short name of the solution type e.g. 'RAP', 'ULT', 'FIN', 'SNX'. Though not part of the + official standard, 'UNK' can also be used to indicate an unknown solution type. + """ + if name is None or len(name.strip()) == 0: + raise ValueError("Solution type name passed was None or effectively empty!", name) + if len(name) > 3: + raise ValueError("Long solution type names are not supported here. Please use RAP, ULT, etc.", name) + name = name.upper() + for solution_type in SolutionTypes._all: + if name == solution_type.name: + return solution_type + raise ValueError(f"No known solution type with short name '{name}'") diff --git a/tests/test_solution_type.py b/tests/test_solution_type.py new file mode 100644 index 0000000..55edde5 --- /dev/null +++ b/tests/test_solution_type.py @@ -0,0 +1,51 @@ +import unittest + +from gnssanalysis.solution_types import SolutionType, SolutionTypes + + +class TestSolutionType(unittest.TestCase): + def test_shortname_to_solution_type(self): + self.assertEqual(SolutionTypes.from_name("ULT"), SolutionTypes.ULT) + self.assertEqual(SolutionTypes.from_name("RAP"), SolutionTypes.RAP) + self.assertEqual(SolutionTypes.from_name("UNK"), SolutionTypes.UNK) + # AssertRaises can be used either as a context manager, or by breaking out the function arguments. + # Note we're not *calling* the function, we're passing the function *so it can be called* by the handler. + self.assertRaises(ValueError, SolutionTypes.from_name, name="noo") + self.assertRaises(ValueError, SolutionTypes.from_name, name="rapid") + self.assertRaises(ValueError, SolutionTypes.from_name, name="") + self.assertRaises(ValueError, SolutionTypes.from_name, name=" ") + + def test_immutability(self): + def update_base_attribute(): + SolutionType.name = "someNewValue" + + # Note that *contents* of a list can still be modified, despite the more general + # protections provided by the metaclass + + def update_enum_attribute_new(): + SolutionTypes.ULT.name = "someBogusValue" + + self.assertRaises(AttributeError, update_base_attribute) + self.assertRaises(AttributeError, update_enum_attribute_new) + + def instantiate_solution_generic(): + SolutionType() + + def instantiate_solution_specific(): + SolutionTypes.RAP() + + def instantiate_solution_helper(): + SolutionTypes() + + self.assertRaises(Exception, instantiate_solution_generic) + self.assertRaises(Exception, instantiate_solution_specific) + self.assertRaises(Exception, instantiate_solution_helper) + + def test_equality(self): + self.assertEqual(SolutionTypes.RAP, SolutionTypes.RAP, "References to same solution type class should be equal") + self.assertEqual( + SolutionTypes.from_name("RAP"), + SolutionTypes.from_name("rap"), + "from_name should give equal results each time, also regardless of case of input", + ) + self.assertNotEqual(SolutionTypes.RAP, SolutionTypes.UNK, "Non-matching solution types should be unequal")