Source code for metawards._ward

from __future__ import annotations

from ._wardinfo import WardInfo

from typing import Union as _Union
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ._wards import Wards

__all__ = ["Ward"]


def _as_positive_integer(number: int, zero_allowed: bool = True):
    try:
        number = int(number)
    except Exception:
        raise ValueError(f"{number} is not an integer")

    if number < 0:
        raise ValueError(f"{number} is negative - it must be positive")

    if number == 0 and not zero_allowed:
        raise ValueError("This value cannot be equal to zero")

    return number


def _as_positive_float(number: float):
    try:
        number = float(number)
    except Exception:
        raise ValueError(f"{number} is not a floating point number")

    if number < 0:
        raise ValueError(f"{number} is not greater or equal to zero")

    return number


[docs]class Ward: """This class holds all of the information about a Ward. It is used to create and edit Networks """
[docs] def __init__(self, id: int = None, name: str = None, code: str = None, authority: str = None, region: str = None, info: WardInfo = None, auto_assign_players: bool = True): """Construct a new Ward, optionally supplying information about the ward Parameters ---------- id: int The identifier for the ward. This must be an integer that is greater or equal to 1. Every ward in a network must have a unique ID. Typically the IDs should be contiguous, as this ID is used as the index into the Network array name: str The name of this ward. This can be used to find the ward. code: str The code of this ward. This is used when wards are identified by codes, rather than names authority: str The local authority name for this node region: str The name of the region containing this ward info: WardInfo The complete WardInfo used to identify this ward. auto_assign_players: bool Whether or not to automatically ensure that all remaining player weight is assigned to players who play in their home ward """ if id is None: self._id = None elif isinstance(id, str): try: self._id = _as_positive_integer(str(id), zero_allowed=False) except Exception: if name is None: name = id self._id = None else: raise TypeError(f"The ID must be an integer - not {id}") else: self._id = _as_positive_integer(id, zero_allowed=False) if info is not None: if not isinstance(info, WardInfo): raise TypeError( f"The passed WardInfo {info} must be of type WardInfo") from copy import deepcopy self._info = deepcopy(info) else: self._info = WardInfo() if name is not None: self._info.name = str(name) if code is not None: self._info.code = str(code) if authority is not None: self._info.authority = str(authority) if region is not None: self._info.region = str(region) if self._id is None and self._info.is_null(): return self._workers = {} self._players = {} self._player_total = 1.0 self._num_workers = 0 self._num_players = 0 self._pos = {} self._scale_uv = 1.0 self._cutoff = 99999.99 self._bg_foi = 0.0 self._custom_params = {} # use this nomenclature to ensure this is a True/False data type self._auto_assign_players = True if auto_assign_players else False
[docs] def __str__(self): if self.is_null(): return "Ward::null" else: idstr = [] if not self._info.is_null(): idstr.append("info=" + self._info.summary()) if self._id is not None: idstr.append("id=" + str(self._id)) idstr = ", ".join(idstr) return f"Ward( {idstr}, " \ f"num_workers={self.num_workers()}, " \ f"num_players={self.num_players()} )"
[docs] def __repr__(self): return self.__str__()
[docs] def __eq__(self, other): return self.__class__ == other.__class__ and \ self.__dict__ == other.__dict__
def __add__(self, other): if isinstance(other, Ward): from ._wards import Wards w = Wards() w.add(self) w.add(other) return w else: raise NotImplementedError()
[docs] def __mul__(self, scale: float) -> Ward: """Scale the number of workers and players by 'scale'""" return self.scale(work_ratio=scale, play_ratio=scale)
[docs] def __rmul__(self, scale: float) -> Ward: """Scale the number of workers and players by 'scale'""" return self.scale(work_ratio=scale, play_ratio=scale)
[docs] def __imul__(self, scale: float) -> Ward: """In-place multiply the number of workers and players by 'scale'""" return self.scale(work_ratio=scale, play_ratio=scale, _inplace=True)
def is_null(self): return self._id is None and self._info.is_null()
[docs] def id(self): """Return the ID of this ward""" return self._id
[docs] def merge(self, other) -> None: """Merge in the data from 'other' into this Ward. The 'info' and the ID numbers must match (or be None). This will add the workers from 'other' to this ward, plus will average the player weights between the two This will do nothing to the scale_uv, cutoff, bg_foi or custom parameters """ if self._info != other._info: raise ValueError( f"Cannot merge as infos are different: {self._info} versus " f"{other._info}") if self._id is None: self._id = other._id if other._id is not None and self._id != other._id: raise ValueError( f"Cannot merge as ID numbers are different: {self._id} versus " f"{other._id}") if self._pos is None: self._pos = other._pos self._num_workers += other._num_workers self._num_players += other._num_players for key, value in other._workers.items(): if key in self._workers: self._workers[key] += value else: self._workers[key] = value assert sum(self._workers.values()) == self._num_workers for key, value in self._players.items(): self._players[key] = 0.5 * self._players[key] for key, value in other._players.items(): self._players[key] = self._players.get(key, 0.0) + (0.5 * value) self._player_total = 1.0 - sum(self._players.values()) assert self._player_total >= 0.0
[docs] def auto_assign_players(self) -> bool: """Return whether or not the remaining player weight is automatically added to the home-ward player weight """ return self._auto_assign_players
[docs] def set_auto_assign_players(self, auto_assign_players: bool = True): """Return whether or not the remaining player weight is automatically assigned to the home-ward player weight """ # use this syntax to ensure True or False type self._auto_assign_players = True if auto_assign_players else False
[docs] def name(self): """Return the name of this ward""" return self._info.name
[docs] def code(self): """Return the code of this ward""" return self._info.code
[docs] def authority(self): """Return the local authority of this ward""" return self._info.authority
[docs] def region(self): """Return the region containing this ward""" return self._info.region
[docs] def info(self): """Return (a copy of) the WardInfo containing all ward identifying metadata """ from copy import deepcopy return deepcopy(self._info)
[docs] def scale_uv(self) -> float: """Return the scale_uv parameter for this ward. This is the amount by which to scale the force of infection (FOI) during the FOI calculation """ return self._scale_uv
[docs] def cutoff(self) -> float: """Return the cutoff parameter for this ward. This sets the maximum distance that residents (or travellers) to this ward can travel """ return self._cutoff
[docs] def bg_foi(self) -> float: """Return the background force of infection (FOI). This is the starting value for ward FOI calculations. It defaults to 0.0. Positive values are used when you want the ward to have some background effect that drives infections independently of the number of infecteds. Negative values imply a background effect that reduces the risk of infection, regardless of the number of infecteds """ return self._bg_foi
[docs] def custom(self, key: str, default: float = 0.0) -> float: """Return the per-ward custom parameter at key 'key', returning 'default' if this has not been set. Note that all custom parameters are stored as floats """ return self._custom_params.get(key, float(default))
[docs] def assert_sane(self): """Assert that the data in this ward is sane""" t = sum(self._players.values()) + self._player_total if abs(t - 1.0) > 1e-6: raise AssertionError(f"Player sum should equal 1.0, not {t}") t = sum(self._workers.values()) if abs(t - self._num_workers) > 1e-6: raise AssertionError( f"Worker sum should be {self._num_workers}, not {t}") if self._scale_uv < 0: raise AssertionError( f"scale_uv must be positive, not {self._scale_uv}") if self._cutoff < 0: raise AssertionError( f"cutoff must be positive, not {self._cutoff}")
[docs] def set_name(self, name: str): """Set the name of this ward""" self._info.name = str(name)
[docs] def set_id(self, id: int): """Set the ID of this ward""" try: id = int(id) except Exception: raise TypeError(f"The ID {id} must be an integer") if id < 1: raise ValueError( f"The passed ID {id} must be greater or equal to 1") if id == self._id: # nothing to do return if id in self._workers or id in self._players: raise ValueError( f"You cannot change the ID {id} to match an ID of an " f"existing link!") old_id = self._id self._id = id if old_id is not None: for key in list(self._workers.keys()): if key == old_id: self._workers[id] = self._workers[old_id] del self._workers[old_id] for key in list(self._players.keys()): if key == old_id: self._players[id] = self._players[old_id] del self._players[old_id] if id not in self._workers: for key in list(self._workers.keys()): if key == self._info: self._workers[id] = self._workers[self._info] del self._workers[self._info] if id not in self._players: for key in list(self._players.keys()): if key == self._info: self._players[id] = self._players[self._info] del self._players[self._info]
[docs] def set_code(self, code: str): """Set the code of this ward""" self._info.code = str(code)
[docs] def set_authority(self, authority: str): """Set the local authority of this ward""" self._info.authority = str(authority)
[docs] def set_region(self, region: str): """Set the region of this ward""" self._info.region = str(region)
[docs] def set_scale_uv(self, scale_uv: float): """Set the scale_uv parameter for this ward. This is the amount by which to scale the FOI when caclulating the FOI. This defaults to 1.0 """ scale_uv = float(scale_uv) if scale_uv < 0: raise ValueError( f"You cannot set scale_uv to a negative value: {scale_uv}") self._scale_uv = scale_uv
[docs] def set_cutoff(self, cutoff: float): """Set the cutoff distance (in km) for this ward. This is the maximum distance that residents (or travellers to) this ward are allowed to travel. This defaults to 99999.99, which is larger than any point to point distance on earth """ cutoff = float(cutoff) if cutoff < 0: raise ValueError( f"You cannot set cutoff to a negative value: {cutoff}") self._cutoff = cutoff
[docs] def set_bg_foi(self, bg_foi: float): """Set the background force of infection (FOI). This is the starting value for ward FOI calculations. It defaults to 0.0. Positive values are used when you want the ward to have some background effect that drives infections independently of the number of infecteds. Negative values imply a background effect that reduces the risk of infection, regardless of the number of infecteds """ bg_foi = float(bg_foi) from math import isnan if isnan(bg_foi): raise ValueError("You cannot set bg_foi to NaN") self._bg_foi = bg_foi
[docs] def set_custom(self, key: str, value: float): """Set the value of the custom ward parameter to 'value'. Note that this must be convertible to a float as all custom parameters are stored as floats """ self._custom_params[key] = float(value)
[docs] def set_info(self, info: WardInfo): """Set the info of this ward""" if not isinstance(info, WardInfo): raise TypeError(f"The ward info {info} must be a WardInfo object") from copy import deepcopy self._info = deepcopy(info)
def _resolve_destination(self, destination: _Union[int, WardInfo] = None): """Resolve the passed destination into either an ID or a WardInfo object """ if destination is None: if self._id is None: if self._info.is_null(): raise ValueError("You cannot add to a null destination") destination = self._info else: destination = self._id elif isinstance(destination, Ward): if destination.id() is None: destination = destination._info if self._id is not None and destination == self._info: # this is this ward destination = self._id else: if destination.id() == self._id: if self._info != destination._info: raise ValueError( f"Disagreement in info for Ward {self}: " f"{self._info} versus {destination._info}.") destination = destination.id() if not isinstance(destination, WardInfo): destination = _as_positive_integer(destination, zero_allowed=False) return destination
[docs] def dereference(self, wards: Wards, _inplace: bool = False) -> Ward: """Dereference the IDs and convert those back to WardInfo objects. This is the opposite of self.resolve(wards). This will return the dereferenced ward. """ from ._wards import Wards if not isinstance(wards, Wards): raise TypeError( f"You can only dereference links using a valid Wards object!") workers = {} players = {} for key, value in self._workers.items(): info = wards.getinfo(key) if info in workers: raise AssertionError(f"Duplicate worker info {key} = {info}") workers[info] = value for key, value in self._players.items(): info = wards.getinfo(key) if info in players: raise AssertionError(f"Duplicate player info {key} = {info}") players[info] = value if _inplace: ward = self else: from copy import deepcopy ward = deepcopy(self) ward._id = None ward._workers = workers ward._players = players return ward
[docs] def resolve(self, wards: Wards, _inplace: bool = None) -> Ward: """Resolve any unresolved links using the passed Wards object 'wards' """ from ._wards import Wards if self.is_resolved(): return if not isinstance(wards, Wards): raise TypeError( f"You can only resolve links using a valid Wards object!") workers = {} players = {} for key, value in self._workers.items(): if isinstance(key, int) or key not in wards: workers[key] = workers.get(key, 0) + value else: idx = wards.index(key) workers[idx] = workers.get(idx, 0) + value for key, value in self._players.items(): if isinstance(key, int) or key not in wards: players[key] = players.get(key, 0.0) + value else: idx = wards.index(key) players[idx] = players.get(idx, 0.0) + value if _inplace: ward = self else: from copy import deepcopy ward = deepcopy(self) if ward._info is not None and ward._info in wards: idx = wards.index(ward._info) if ward._id is not None and idx != self._id: raise KeyError(f"Wrong ID for {ward}? {idx}") ward._id = idx ward._players = players ward._workers = workers return ward
[docs] def is_resolved(self): """Return whether or not any of the worker or player links in this ward are unresolved, or whether the ID of this ward is None """ if self._id is None: return False for key in self._workers.keys(): if not isinstance(key, int): return False for key in self._players.keys(): if not isinstance(key, int): return False return True
[docs] def depopulate(self, zero_player_weights: bool = False) -> Ward: """Return a copy of this Ward with exact same details, but with zero population. If 'zero_player_weights' is True, then this will also zero the player weights for all connected wards """ from copy import deepcopy result = deepcopy(self) result._num_workers = 0 result._num_players = 0 for key in self._workers.keys(): result._workers[key] = 0 if zero_player_weights: for key in self._players.keys(): result._players[key] = 0.0 result._player_total = 1.0 return result
def _harmonise_links(self, other: Ward) -> None: """Make sure that this ward has exactly the same links as 'other'. This is used as part of the Wards.harmonise function, and ensures that all subnet wards have identical ward and link indexes. This is always performed in-place """ errors = [] if self._id != other._id: errors.append(f"Disagreement in Ward ID number: {self._id} versus " f"{other._id}") if self._info != other._info: errors.append(f"Disagreement in Ward info: {self._info} versus " f"{other._info}") for key in self._workers.keys(): if key not in other._workers: errors.append(f"Missing work link to {key}") for key in self._players.keys(): if key not in other._players: errors.append(f"Missing play link to {key}") if len(errors) > 0: from .utils._console import Console Console.error("\n".join(errors)) raise ValueError( f"Cannot harmonise links of incompatible wards: {self._id}") for key in other._workers.keys(): if key not in self._workers: self._workers[key] = 0 for key in other._players.keys(): if key not in self._players: self._players[key] = 0.0
[docs] def add_workers(self, number: int, destination: _Union[int, WardInfo] = None): """Add some workers to this ward, specifying their destination if they work out of ward """ destination = self._resolve_destination(destination) number = _as_positive_integer(number) if destination not in self._workers: self._workers[destination] = 0 self._workers[destination] += number self._num_workers += number
[docs] def subtract_workers(self, number: int, destination: int = None): """Remove some workers from this ward, specifying their destination if they work out of ward """ destination = self._resolve_destination(destination) number = _as_positive_integer(number) if destination not in self._workers: return if number >= self._workers[destination]: self._num_workers -= self._workers[destination] del self._workers[destination] else: self._workers -= number self._num_workers -= number
[docs] def add_player_weight(self, weight: float, destination: int = None): """Add the weight for players who will randomly move to the specified destination to play(or to play in the home ward if destination is not set). Note that the sum of player weights cannot be greater than 1.0. """ tiny = 1e-10 weight = _as_positive_float(weight) destination = self._resolve_destination(destination) if weight < tiny: return if abs(weight - self._player_total) < tiny: weight = self._player_total if weight > self._player_total: raise ValueError( f"You cannot add {weight} to {destination} as the sum of " f"weights must be <= 1.0, and this is greater " f"than the remaining weight available {self._player_total}. " f"You can only add a weight that is less than this value") if destination not in self._players: self._players[destination] = 0 self._players[destination] += weight self._player_total -= weight if self._player_total < tiny: self._player_total = 0
[docs] def subtract_player_weight(self, weight: float, destination: int = None): """Subtract the weight for players who will randomly move to the specified destination to play (or to play in the home ward if destination is not set). """ tiny = 1e-10 weight = _as_positive_float(weight) destination = self._resolve_destination(destination) if weight < tiny: return if destination not in self._players: return if weight > self._players[destination]: weight = self._players[destination] self._player_total += weight del self._players[destination] if abs(self._player_total - 1.0) < tiny: self._player_total = 1.0
[docs] def get_workers(self, destination: int = None): """Return the number of workers who commute to the specified destination ward (or who commute to their home ward if destination is not set) """ destination = self._resolve_destination(destination) return self._workers.get(destination, 0)
[docs] def get_players(self, destination: int = None): """Return the fraction of players who will play in the specified destination ward (or who play in their home ward if destination is not set) """ destination = self._resolve_destination(destination) p = self._players.get(destination, 0.0) if self._auto_assign_players and destination == self._id: p += self._player_total return p
[docs] def num_workers(self): """Return the total number of workers who make their home in this ward """ return self._num_workers
[docs] def num_players(self): """Return the total number of players who make their home in this ward """ return self._num_players
[docs] def population(self): """Return the total population of this ward""" return self._num_workers + self._num_players
[docs] def set_num_players(self, number: int): """Set the number of players in this ward""" self._num_players = _as_positive_integer(number)
[docs] def set_num_workers(self, number: int): """Set the number of workers in this ward.""" number = _as_positive_integer(number) delta = number - self._num_workers if delta > 0: # add the workers as individuals who commute to their home ward self.add_workers(delta) elif delta < 0: # can we remove just the workers who commute to their home ward? if delta <= self.get_workers(): self.subtract_workers(delta) else: raise ValueError( f"Cannot set the number of workers to {number} as there " f"are not enough home workers to subtract")
[docs] def get_worker_lists(self): """Return a pair of arrays, containing the destination wards and worker populations for this ward """ from .utils._array import create_int_array keys = list(self._workers.keys()) keys.sort() wards = create_int_array(len(keys)) pops = create_int_array(len(keys)) for i, key in enumerate(keys): if not isinstance(key, int): raise KeyError( f"Cannot create worker list as link to {key} is " f"unresolved") wards[i] = key pops[i] = self._workers[key] return (wards, pops)
[docs] def get_player_lists(self, no_auto_assign: bool = False): """Return a pair of arrays, containing the destination wards and player weights for this ward. If 'no_auto_assign' is set then do not include any auto-assigned weights. This normally should be false, as it is only used when serialising """ from .utils._array import create_double_array, create_int_array keys = list(self._players.keys()) auto_assign = (not no_auto_assign) and self._auto_assign_players if auto_assign and self._id not in keys: keys.append(self._id) keys.sort() wards = create_int_array(len(keys)) weights = create_double_array(len(keys)) for i, key in enumerate(keys): if not isinstance(key, int): raise KeyError( f"Cannot create worker list as link to {key} is " f"unresolved") wards[i] = key weights[i] = self._players.get(key, 0.0) if auto_assign and key == self._id: weights[i] += self._player_total return (wards, weights)
[docs] def set_position(self, x: float = None, y: float = None, lat: float = None, long: float = None, units: str = "m"): """Set the position of the centre of this ward. This can be set either as x/y coordinates or latitude/longitude coordinates. Parameters ---------- x: float The x coordinates if x/y are used. The units are set via the 'units' argument y: float The y coordinates if x/y are used. The units are set via the 'units' argument units: str The units for x/y coordinates. This should be "m" for meters or "km" for kilometers lat: float The latitude if lat/long coordinates are used long: float The longitude if lat/long coordinates are used """ if x is not None: if y is None or lat is not None or long is not None: raise ValueError( f"You must set either x/y or lat/long, but not both!") if units is None: units = "m" units = str(units).strip().lower() if units in ["m", "meter", "meters"]: units = 0.001 elif units in ["km", "kilometer", "kilometers"]: units = 1.0 else: raise ValueError( f"Unrecognised units {units}. This should be either " f"'m' or 'km'") self._pos = {"x": units * float(x), "y": units * float(y)} elif lat is not None: if long is None or x is not None or y is not None: raise ValueError( f"You must set either x/y or lat/long, but not both!") self._pos = {"lat": float(lat), "long": float(long)} else: # nothing is being set - ignore if y is not None or long is not None: raise ValueError( "Confused inputs. Either set x and y, or lat and long")
[docs] def position(self): """Return the position of the center of this ward. This will return a dictionary containing either the {"x", "y"} coordinates (in kilometers) or containing the {"lat", "long"} coordinates, or an empty dictionary if cooordinates have not been set. """ from copy import deepcopy return deepcopy(self._pos)
[docs] def work_connections(self): """Return the full list of work connections for this ward""" c = list(self._workers.keys()) try: c.sort() except Exception: pass return c
[docs] def play_connections(self): """Return the full list of play connections for this ward""" c = list(self._players.keys()) try: c.sort() except Exception: pass return c
[docs] def scale(self, work_ratio: float = 1.0, play_ratio: float = 1.0, _inplace: bool = False) -> Ward: """Return a copy of these wards where the number of workers and players have been scaled by 'work_ratios' and 'play_ratios' respectively. These can be greater than 1.0, e.g. if you want to scale up the number of workers and players Parameters ---------- work_ratio: float The scaling ratio for workers play_ratio: float The scaling ratio for players Returns ------- Wards: A copy of this Wards scaled by the requested amount """ work_ratio = float(work_ratio) play_ratio = float(play_ratio) if _inplace: ward = self else: from copy import deepcopy ward = deepcopy(self) def scale_and_round(value, scale): import math if scale > 0.5: # round up for large scales, as smaller scales will always # round down return int(math.floor((value * scale) + 0.5)) else: # rounding down - hopefully this will minimise the number # of values that need to be redistributed return int(math.floor(value * scale)) if play_ratio != 1.0: ward._num_players = scale_and_round(ward._num_players, play_ratio) if work_ratio != 1.0: for key, value in ward._workers.items(): ward._workers[key] = scale_and_round(value, work_ratio) ward._num_workers = ward._num_workers = sum(ward._workers.values()) return ward
[docs] def to_data(self): """Return a dictionary that can be serialised to JSON""" data = {} data["id"] = self._id if len(self._pos) > 0: data["position"] = self.position() data["info"] = self._info.to_data() if not self._auto_assign_players: data["auto_assign_players"] = False data["num_workers"] = self.num_workers() data["num_players"] = self.num_players() workers = self.get_worker_lists() if len(workers[0]) > 0: data["workers"] = {"destination": workers[0].tolist(), "population": workers[1].tolist()} players = self.get_player_lists(no_auto_assign=True) if len(players[0]) > 0: data["players"] = {"destination": players[0].tolist(), "weights": players[1].tolist()} if self._scale_uv != 1.0: data["scale_uv"] = self._scale_uv if self._cutoff != 99999.99: data["cutoff"] = self._cutoff if self._bg_foi != 0.0: data["bg_foi"] = self._bg_foi if len(self._custom_params) > 0: from copy import deepcopy data["custom"] = deepcopy(self._custom_params) return data
[docs] @staticmethod def from_data(data): """Return a Ward that has been created from the passed dictionary (e.g. which has been deserialised from JSON) """ if data is None or len(data) == 0: return Ward() ward = Ward(id=data.get("id", None), info=WardInfo.from_data(data.get("info", {})), auto_assign_players=True) pos = data.get("position", {}) ward.set_position(x=pos.get("x", None), y=pos.get("y", None), lat=pos.get("lat", None), long=pos.get("long", None), units="km") ward.set_auto_assign_players(data.get("auto_assign_players", True)) ward.set_num_players(data.get("num_players", 0)) workers = data.get("workers", {}) for d, p in zip(workers.get("destination", []), workers.get("population", [])): d = _as_positive_integer(d, zero_allowed=False) p = _as_positive_integer(p) ward._workers[d] = p ward._num_workers = sum(ward._workers.values()) if data.get("num_workers", 0) > 0: if ward.num_workers() != data["num_workers"]: raise AssertionError( f"Disagreement in number of workers: {ward.num_workers()} " f"versus {data['num_workers']}") players = data.get("players", {}) for d, w in zip(players.get("destination", []), players.get("weights", [])): d = _as_positive_integer(d, zero_allowed=False) w = _as_positive_float(w) ward._players[d] = w ward._player_total = 1.0 - sum(ward._players.values()) if abs(ward._player_total) < 1e-10: ward._player_total = 0 elif ward._player_total < 0: raise AssertionError( f"The sum of player weights cannot be greater than zero") ward._scale_uv = float(data.get("scale_uv", 1.0)) ward._cutoff = float(data.get("cutoff", 99999.99)) ward._bg_foi = float(data.get("bg_foi", 0.0)) for key, value in data.get("custom", {}).items(): ward._custom_params[key] = float(value) ward.assert_sane() return ward
[docs] def to_json(self, filename: str = None, indent: int = None, auto_bzip: bool = True) -> str: """Serialise the ward to JSON. This will write to a file if filename is set, otherwise it will return a JSON string. Parameters ---------- filename: str The name of the file to write the JSON to. The absolute path to the written file will be returned. If filename is None then this will serialise to a JSON string which will be returned. indent: int The number of spaces of indent to use when writing the json auto_bzip: bool Whether or not to automatically bzip2 the written json file Returns ------- str Returns either the absolute path to the written file, or the json-serialised string """ import json if indent is not None: indent = int(indent) if filename is None: return json.dumps(self.to_data(), indent=indent) else: from pathlib import Path filename = str(Path(filename).expanduser().resolve().absolute()) if auto_bzip: if not filename.endswith(".bz2"): filename += ".bz2" import bz2 with bz2.open(filename, "wt") as FILE: try: json.dump(self.to_data(), FILE, indent=indent) except Exception: import os FILE.close() os.unlink(filename) raise else: with open(filename, "w") as FILE: try: json.dump(self.to_data(), FILE, indent=indent) except Exception: import os FILE.close() os.unlink(filename) raise return filename
[docs] @staticmethod def from_json(s: str): """Return the Ward constructed from the passed json. This will either load from a passed json string, or from json loaded from the passed file """ import os import json if os.path.exists(s): try: import bz2 with bz2.open(s, "rt") as FILE: data = json.load(FILE) except Exception: data = None if data is None: with open(s, "rt") as FILE: data = json.load(FILE) else: try: data = json.loads(s) except Exception: data = None if data is None: from .utils._console import Console Console.error(f"Unable to load a Ward from '{s}'. Check that " f"this is valid JSON or that the file exists.") raise IOError(f"Cannot load Wards from '{s}'") return Ward.from_data(data)