diff --git a/VisualCrossing/_api.py b/VisualCrossing/_api.py new file mode 100644 index 0000000..84d727b --- /dev/null +++ b/VisualCrossing/_api.py @@ -0,0 +1,169 @@ +# -*- encoding: utf-8 -*- + +import json +import time +import platform +import warnings + +from time import ctime +from uuid import uuid4 +from sys import modules +from shutil import copy +from os.path import join +from pathlib import Path + +from . import ( + __name__, + __version__, + __homepath__, + ) +from .errors import * # noqa: F403 +from ._config import config + +class base(object): + """A base class that wraps essential utilities for :class:`API` + + :class:`API` is a python wrapper, to fetch API data from VisualCrossing, + which can be used to fetch historic as well as forecasted weather data. + """ + + def __init__(self, **kwargs): + status, response = config() + if status == 100: + # use defaults + response = self.__args_default__() + + for k, v in response.items(): + setattr(self, k, v) + + + def __args_default__(self) -> dict: + """Defines a Dictionary of Default Values for Keyword Arguments (or Attributes)""" + + return { + "unitGroup" : "metric", + "contentType" : "csv", + "aggregateHours" : 24 + } + + + def __get_args_default__(self, args : str): + """Get the Default Value associated with a Keyword Argument""" + + return self.__args_default__().get(args, None) # None if key not available + + + @property + def __optional_args__(self): + """Get List of all the Optional Keyword Arguments Accepted by the API""" + + return self.__args_default__().keys() + + + def generate_config( + self, + defaultSettings : bool = True, + fileName : str = "config.json", + overwrite : bool = False, + keepBackup : bool = True, + **kwargs + ) -> bool: + """Generate configuration file at `__homepath__` when executed + + The configuration file can be generated with default settings as defined at + :func:`__args_default__` else, user is requested to pass all necessary settings + in a correct format (as required by API) to the function, setting `key` as the + attribute name, and `value` as the desired value. Users are also advised not to + save the `API_KEY` in the configuration file (for security purpose), and to use + :func:`_generate_key` to save the key file in an encrypted format. + + :param defaultSettings: Should you wish to save the configuration file with + the default settings. If set to `False` then user is + requested to pass all necessary attributes (`key`) and + their values. Defaults to `True`. + + :param fileName: Output file name (with extension - `json`). Defaults to + `config.json`. + + :param overwrite: Overwrite existing configuration file, if exists (same filename). + Defaults to `False`. + + :param keepBackup: If same file name exists, then setting the parameter to `True` will + create a backup of the file with the following format + `..json` where `UUID` is a randomly generated + 7-charecters long name. Defaults to `True`. + + Accepts n-Keyword Arguments, which are all default settings that can be used to initialize + the API. + """ + + outfile = join(__homepath__, fileName) + + # get attributes to write to file + if defaultSettings: + attrs = self.__args_default__() + else: + attrs = kwargs # get all attribute from kwargs + + # re-format attrs to a defined type, with meta-informations + attrs = { + "__header__" : { + "program" : __name__, + "version" : __version__, + "homepath" : __homepath__ + }, + + "platform" : { + "platform" : platform.platform(), + "architecture" : platform.machine(), + "version" : platform.version(), + "system" : platform.system(), + "processor" : platform.processor(), + "uname" : platform.uname() + }, + + "attributes" : attrs, + "timestamp" : ctime() + } + + # lambda to write to json file + # where `kv` is the key-value pair, which is typically `attrs` + # defined/reformatted in the above section + def write_json(kv : dict, file : str): + with open(file, "w") as f: + json.dump(kv, f, sort_keys = False, indent = 4, default = str) + + if Path(outfile).is_file(): # file exists + warnings.warn(f"{outfile} already exists.", FileExists) + if keepBackup: + try: + name, extension = fileName.split(".") + except ValueError as err: + name = fileName.split(".")[0] + extension = "json" + warnings.warn(f"{fileName} is not of proper type. Setting name as: {name}", ImproperFileName) + + new_file = ".".join([name, str(uuid4())[:7], extension]) + + # copy to a new file + print(f"Old configuration file is available at {new_file}") + try: + # python 3.8+ + copy(outfile, join(__homepath__, new_file)) + except TypeError: + # python <= 3.7 + # https://stackoverflow.com/a/33626207/6623589 + copy(str(outfile), str(join(__homepath__, new_file))) + else: + warnings.warn(f"{outfile} will be overwritten without a backup.", FileExists) + + if overwrite: + write_json(attrs, outfile) # write to file + else: + raise ValueError(f"{outfile} already exists, and `overwrite` is set to `False`") + + else: + # same file does not exists, other parameters are not required for validation + write_json(attrs, outfile) # write to file + + return True diff --git a/VisualCrossing/_config.py b/VisualCrossing/_config.py new file mode 100644 index 0000000..e977498 --- /dev/null +++ b/VisualCrossing/_config.py @@ -0,0 +1,53 @@ +# -*- encoding: utf-8 -*- + +from json import load + +# set configuration option +def config(file : str = None) -> dict: + """Read a defined configuration file from home-path, + and set it to be used module wide.""" + + if file: + file = join(__homepath__, file) + if not Path(file).is_file(): + raise FileNotFoundError(f"File {file} is not available.") + else: + status = 200 # use from configuration file + + # file exists, read as a json file + params = load(open(file, "r")) # read as dictionary + + # check if the formatting is correct + if "attributes" not in params.keys(): + from .errors import WrongConfigFile + raise WrongConfigFile("Configuration file is wrong, check documentation.") + else: + # additionally, check other defined keys (as in generate file) + # is present, which ensures data integrity + optional = [] + for k in ["__header__", "platform", "timestamp"]: + if k not in params.keys(): + optional.append(k) + + if optional: + import warnings + from .errors import VerificationWarning + + warnings.warn(f"Config file missing {optional}", VerificationWarning) + params = params["attributes"] + + else: + status = 100 # continue + params = None # use defualts + + return status, params + + # # set attribute to default class + # for k, v in params.items(): + # globals()[k] = v + +# # always initialize api with default values +# # once initialized, all the attributes are now +# # available as module level attrbute, thus the same +# # can be referenced as `VisualCrossing.unitGroup` +# config() diff --git a/VisualCrossing/api.py b/VisualCrossing/api.py index 2a94e7b..fedf13f 100644 --- a/VisualCrossing/api.py +++ b/VisualCrossing/api.py @@ -10,6 +10,7 @@ from datetime import datetime as dt from requests.exceptions import SSLError +from ._api import base from .errors import * # noqa: F403 class _base(object): @@ -28,7 +29,7 @@ def __valid_keys__(self): return ["key", "unitGroup", "contentType"] -class API(_base): +class API(base): """A basic API for Visual-Crossing Weather Data :param date: Date for which weather data is required. Pass the date in `YYYY-MM-DD` format, @@ -63,7 +64,7 @@ def __init__( # define keyword arguments self.endDate = kwargs.get("endDate", None) - self.unitGroup = kwargs.get("unitGroup", "metric") + # self.unitGroup = kwargs.get("unitGroup", "metric") self.contentType = kwargs.get("contentType", "csv") self.aggregateHours = kwargs.get("aggregateHours", 24) diff --git a/VisualCrossing/errors.py b/VisualCrossing/errors.py index 4dda1c2..5d1f6c3 100644 --- a/VisualCrossing/errors.py +++ b/VisualCrossing/errors.py @@ -5,8 +5,17 @@ class WrongDateFormat(Exception): class VerificationWarning(Warning): - """Warning is raised when fetching data with flag `verify = False`""" + """Warning is raised when fetching data with flag `verify = False` or config file not verified""" class NoDataFetched(Exception): """Exception is raised when data is not fetched, i.e. received `status_code != 200`""" + +class ImproperFileName(Warning): + """Warning is raised when a file name is received which is not of format `name.extension`""" + +class FileExists(Warning): + """Warning is raised when a given file name already exists, and needs to be modified""" + +class WrongConfigFile(Exception): + """Error is raised when the given configuration file does not have a key named `attributes`""" \ No newline at end of file