summaryrefslogtreecommitdiff
path: root/meta
diff options
context:
space:
mode:
authorSefa Eyeoglu <contact@scrumplex.net>2022-03-21 21:34:52 +0100
committerSefa Eyeoglu <contact@scrumplex.net>2022-04-05 14:30:30 +0200
commita8babf42c5176fc7c86d60caa2fc56c0abbf2e04 (patch)
treefc698f7d4d8c91406ad52440ebedfa2366b0c0da /meta
parent0731956adc560616fcbe8ea24d6dc3a49e3ad0c6 (diff)
downloadProject-Tick-a8babf42c5176fc7c86d60caa2fc56c0abbf2e04.tar.gz
Project-Tick-a8babf42c5176fc7c86d60caa2fc56c0abbf2e04.zip
refactor: move utils into package
Diffstat (limited to 'meta')
-rw-r--r--meta/__init__.py1
-rw-r--r--meta/common.py19
-rw-r--r--meta/fabricutil.py32
-rw-r--r--meta/forgeutil.py323
-rw-r--r--meta/jsonobject/__init__.py17
-rw-r--r--meta/jsonobject/api.py53
-rw-r--r--meta/jsonobject/base.py394
-rw-r--r--meta/jsonobject/base_properties.py320
-rw-r--r--meta/jsonobject/containers.py252
-rw-r--r--meta/jsonobject/exceptions.py10
-rw-r--r--meta/jsonobject/properties.py155
-rw-r--r--meta/jsonobject/utils.py57
-rw-r--r--meta/liteloaderutil.py118
-rw-r--r--meta/metautil.py477
14 files changed, 2228 insertions, 0 deletions
diff --git a/meta/__init__.py b/meta/__init__.py
new file mode 100644
index 0000000000..05cee3e7d7
--- /dev/null
+++ b/meta/__init__.py
@@ -0,0 +1 @@
+"""Meta package of meta"""
diff --git a/meta/common.py b/meta/common.py
new file mode 100644
index 0000000000..5454f517f3
--- /dev/null
+++ b/meta/common.py
@@ -0,0 +1,19 @@
+import os
+
+
+def polymc_path():
+ if "PMC_DIR" in os.environ:
+ return os.environ["PMC_DIR"]
+ return "polymc"
+
+
+def upstream_path():
+ if "UPSTREAM_DIR" in os.environ:
+ return os.environ["UPSTREAM_DIR"]
+ return "upstream"
+
+
+def ensure_component_dir(component_id):
+ path = os.path.join(polymc_path(), component_id)
+ if not os.path.exists(path):
+ os.makedirs(path)
diff --git a/meta/fabricutil.py b/meta/fabricutil.py
new file mode 100644
index 0000000000..2f38f8f051
--- /dev/null
+++ b/meta/fabricutil.py
@@ -0,0 +1,32 @@
+from .metautil import *
+
+
+class FabricInstallerArguments(JsonObject):
+ client = ListProperty(StringProperty)
+ common = ListProperty(StringProperty)
+ server = ListProperty(StringProperty)
+
+
+class FabricInstallerLaunchwrapper(JsonObject):
+ tweakers = ObjectProperty(FabricInstallerArguments, required=True)
+
+
+class FabricInstallerLibraries(JsonObject):
+ client = ListProperty(PolyMCLibrary)
+ common = ListProperty(PolyMCLibrary)
+ server = ListProperty(PolyMCLibrary)
+
+
+class FabricInstallerDataV1(JsonObject):
+ version = IntegerProperty(required=True)
+ libraries = ObjectProperty(FabricInstallerLibraries, required=True)
+ mainClass = DefaultProperty()
+ arguments = ObjectProperty(FabricInstallerArguments, required=False)
+ launchwrapper = ObjectProperty(FabricInstallerLaunchwrapper, required=False)
+
+
+class FabricJarInfo(JsonObject):
+ releaseTime = ISOTimestampProperty()
+ size = IntegerProperty()
+ sha256 = StringProperty()
+ sha1 = StringProperty()
diff --git a/meta/forgeutil.py b/meta/forgeutil.py
new file mode 100644
index 0000000000..4c2615cb8d
--- /dev/null
+++ b/meta/forgeutil.py
@@ -0,0 +1,323 @@
+from collections import namedtuple
+
+from .metautil import *
+
+
+# A post-processed entry constructed from the reconstructed Forge version index
+class ForgeVersion:
+ def __init__(self, entry):
+ self.build = entry.build
+ self.rawVersion = entry.version
+ self.mcversion = entry.mcversion
+ self.mcversion_sane = self.mcversion.replace("_pre", "-pre", 1)
+ self.branch = entry.branch
+ self.installer_filename = None
+ self.installer_url = None
+ self.universal_filename = None
+ self.universal_url = None
+ self.changelog_url = None
+ self.longVersion = "%s-%s" % (self.mcversion, self.rawVersion)
+ if self.branch != None:
+ self.longVersion = self.longVersion + "-%s" % (self.branch)
+ for classifier, fileentry in entry.files.items():
+ extension = fileentry.extension
+ checksum = fileentry.hash
+ filename = fileentry.filename(self.longVersion)
+ url = fileentry.url(self.longVersion)
+ if (classifier == "installer") and (extension == "jar"):
+ self.installer_filename = filename
+ self.installer_url = url
+ if (classifier == "universal" or classifier == "client") and (extension == "jar" or extension == "zip"):
+ self.universal_filename = filename
+ self.universal_url = url
+ if (classifier == "changelog") and (extension == "txt"):
+ self.changelog_url = url
+
+ def name(self):
+ return "Forge %d" % (self.build)
+
+ def usesInstaller(self):
+ if self.installer_url == None:
+ return False
+ if self.mcversion == "1.5.2":
+ return False
+ return True
+
+ def filename(self):
+ if self.usesInstaller():
+ return self.installer_filename
+ else:
+ return self.universal_filename
+
+ def url(self):
+ if self.usesInstaller():
+ return self.installer_url
+ else:
+ return self.universal_url
+
+ def isSupported(self):
+ if self.url() == None:
+ return False
+
+ versionElements = self.rawVersion.split('.')
+ if len(versionElements) < 1:
+ return False
+
+ majorVersionStr = versionElements[0]
+ if not majorVersionStr.isnumeric():
+ return False
+
+ # majorVersion = int(majorVersionStr)
+ # if majorVersion >= 37:
+ # return False
+
+ return True
+
+
+class ForgeFile(JsonObject):
+ classifier = StringProperty(required=True)
+ hash = StringProperty(required=True)
+ extension = StringProperty(required=True)
+
+ def filename(self, longversion):
+ return "%s-%s-%s.%s" % ("forge", longversion, self.classifier, self.extension)
+
+ def url(self, longversion):
+ return "https://files.minecraftforge.net/maven/net/minecraftforge/forge/%s/%s" % (
+ longversion, self.filename(longversion))
+
+
+class ForgeEntry(JsonObject):
+ longversion = StringProperty(required=True)
+ mcversion = StringProperty(required=True)
+ version = StringProperty(required=True)
+ build = IntegerProperty(required=True)
+ branch = StringProperty()
+ latest = BooleanProperty()
+ recommended = BooleanProperty()
+ files = DictProperty(ForgeFile)
+
+
+class ForgeMcVersionInfo(JsonObject):
+ latest = StringProperty()
+ recommended = StringProperty()
+ versions = ListProperty(StringProperty())
+
+
+class DerivedForgeIndex(JsonObject):
+ versions = DictProperty(ForgeEntry)
+ by_mcversion = DictProperty(ForgeMcVersionInfo)
+
+
+'''
+FML library mappings - these are added to legacy Forge versions because Forge no longer can download these
+by itself - the locations have changed and some of this has to be rehosted on PolyMC servers.
+'''
+
+FMLLib = namedtuple('FMLLib', ('filename', 'checksum', 'ours'))
+
+fmlLibsMapping = {}
+
+fmlLibsMapping["1.3.2"] = [
+ FMLLib("argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", False),
+ FMLLib("guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", False),
+ FMLLib("asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", False)
+]
+
+fml14 = [
+ FMLLib("argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b", False),
+ FMLLib("guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f", False),
+ FMLLib("asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82", False),
+ FMLLib("bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb", False)
+]
+fmlLibsMapping["1.4"] = fml14;
+fmlLibsMapping["1.4.1"] = fml14;
+fmlLibsMapping["1.4.2"] = fml14;
+fmlLibsMapping["1.4.3"] = fml14;
+fmlLibsMapping["1.4.4"] = fml14;
+fmlLibsMapping["1.4.5"] = fml14;
+fmlLibsMapping["1.4.6"] = fml14;
+fmlLibsMapping["1.4.7"] = fml14;
+
+fmlLibsMapping["1.5"] = [
+ FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", False),
+ FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", False),
+ FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", False),
+ FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", True),
+ FMLLib("deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8", False),
+ FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", True)
+]
+
+fmlLibsMapping["1.5.1"] = [
+ FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", False),
+ FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", False),
+ FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", False),
+ FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", True),
+ FMLLib("deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6", False),
+ FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", True)
+]
+
+fmlLibsMapping["1.5.2"] = [
+ FMLLib("argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51", False),
+ FMLLib("guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a", False),
+ FMLLib("asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58", False),
+ FMLLib("bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65", True),
+ FMLLib("deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9", False),
+ FMLLib("scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85", True)
+]
+
+'''
+"install": {
+ "profileName": "Forge",
+ "target":"Forge8.9.0.753",
+ "path":"net.minecraftforge:minecraftforge:8.9.0.753",
+ "version":"Forge 8.9.0.753",
+ "filePath":"minecraftforge-universal-1.6.1-8.9.0.753.jar",
+ "welcome":"Welcome to the simple Forge installer.",
+ "minecraft":"1.6.1",
+ "logo":"/big_logo.png",
+ "mirrorList": "http://files.minecraftforge.net/mirror-brand.list"
+},
+"install": {
+ "profileName": "forge",
+ "target":"1.11-forge1.11-13.19.0.2141",
+ "path":"net.minecraftforge:forge:1.11-13.19.0.2141",
+ "version":"forge 1.11-13.19.0.2141",
+ "filePath":"forge-1.11-13.19.0.2141-universal.jar",
+ "welcome":"Welcome to the simple forge installer.",
+ "minecraft":"1.11",
+ "mirrorList" : "http://files.minecraftforge.net/mirror-brand.list",
+ "logo":"/big_logo.png",
+ "modList":"none"
+},
+'''
+
+
+class ForgeInstallerProfileInstallSection(JsonObject):
+ profileName = StringProperty(required=True)
+ target = StringProperty(required=True)
+ path = GradleSpecifierProperty(required=True)
+ version = StringProperty(required=True)
+ filePath = StringProperty(required=True)
+ welcome = StringProperty(required=True)
+ minecraft = StringProperty(required=True)
+ logo = StringProperty(required=True)
+ mirrorList = StringProperty(required=True)
+ modList = StringProperty(exclude_if_none=True, default=None)
+
+
+class ForgeLibrary(MojangLibrary):
+ url = StringProperty(exclude_if_none=True)
+ serverreq = BooleanProperty(exclude_if_none=True, default=None)
+ clientreq = BooleanProperty(exclude_if_none=True, default=None)
+ checksums = ListProperty(StringProperty)
+ comment = StringProperty()
+
+
+class ForgeVersionFile(MojangVersionFile):
+ libraries = ListProperty(ForgeLibrary, exclude_if_none=True, default=None) # overrides Mojang libraries
+ inheritsFrom = StringProperty()
+ jar = StringProperty()
+
+
+'''
+"optionals": [
+ {
+ "name": "Mercurius",
+ "client": true,
+ "server": true,
+ "default": true,
+ "inject": true,
+ "desc": "A mod that collects statistics about Minecraft and your system.<br>Useful for Forge to understand how Minecraft/Forge are used.",
+ "url": "http://www.minecraftforge.net/forum/index.php?topic=43278.0",
+ "artifact": "net.minecraftforge:MercuriusUpdater:1.11.2",
+ "maven": "http://files.minecraftforge.net/maven/"
+ }
+]
+'''
+
+
+class ForgeOptional(JsonObject):
+ name = StringProperty()
+ client = BooleanProperty()
+ server = BooleanProperty()
+ default = BooleanProperty()
+ inject = BooleanProperty()
+ desc = StringProperty()
+ url = StringProperty()
+ artifact = GradleSpecifierProperty()
+ maven = StringProperty()
+
+
+class ForgeInstallerProfile(JsonObject):
+ install = ObjectProperty(ForgeInstallerProfileInstallSection, required=True)
+ versionInfo = ObjectProperty(ForgeVersionFile, required=True)
+ optionals = ListProperty(ForgeOptional)
+
+
+class ForgeLegacyInfo(JsonObject):
+ releaseTime = ISOTimestampProperty()
+ size = IntegerProperty()
+ sha256 = StringProperty()
+ sha1 = StringProperty()
+
+
+class ForgeLegacyInfoList(JsonObject):
+ number = DictProperty(ForgeLegacyInfo)
+
+
+class DataSpec(JsonObject):
+ client = StringProperty()
+ server = StringProperty()
+
+
+class ProcessorSpec(JsonObject):
+ jar = StringProperty()
+ classpath = ListProperty(StringProperty)
+ args = ListProperty(StringProperty)
+ outputs = DictProperty(StringProperty)
+ sides = ListProperty(StringProperty, exclude_if_none=True, default=None)
+
+
+# Note: This is only used in one version (1.12.2-14.23.5.2851) and we don't even use the installer profile in it.
+# It's here just so it parses and we can continue...
+class ForgeInstallerProfileV1_5(JsonObject):
+ _comment = ListProperty(StringProperty)
+ spec = IntegerProperty()
+ profile = StringProperty()
+ version = StringProperty()
+ icon = StringProperty()
+ json = StringProperty()
+ path = GradleSpecifierProperty()
+ logo = StringProperty()
+ minecraft = StringProperty()
+ welcome = StringProperty()
+ # We don't know what 'data' actually is in this one. It's an empty array
+ data = ListProperty(StringProperty)
+ processors = ListProperty(ProcessorSpec)
+ libraries = ListProperty(MojangLibrary)
+ mirrorList = StringProperty(exclude_if_none=True, default=None)
+
+
+class ForgeInstallerProfileV2(JsonObject):
+ _comment = ListProperty(StringProperty)
+ spec = IntegerProperty()
+ profile = StringProperty()
+ version = StringProperty()
+ icon = StringProperty()
+ json = StringProperty()
+ path = GradleSpecifierProperty()
+ logo = StringProperty()
+ minecraft = StringProperty()
+ welcome = StringProperty()
+ data = DictProperty(DataSpec)
+ processors = ListProperty(ProcessorSpec)
+ libraries = ListProperty(MojangLibrary)
+ mirrorList = StringProperty(exclude_if_none=True, default=None)
+ serverJarPath = StringProperty(exclude_if_none=True, default=None)
+
+
+class InstallerInfo(JsonObject):
+ sha1hash = StringProperty()
+ sha256hash = StringProperty()
+ size = IntegerProperty()
diff --git a/meta/jsonobject/__init__.py b/meta/jsonobject/__init__.py
new file mode 100644
index 0000000000..83f41913c4
--- /dev/null
+++ b/meta/jsonobject/__init__.py
@@ -0,0 +1,17 @@
+# TODO: maybe move to pydantic in the future?
+
+from __future__ import absolute_import
+from .base import JsonObjectMeta
+from .containers import JsonArray
+from .properties import *
+from .base_properties import *
+from .api import JsonObject
+
+__all__ = [
+ 'IntegerProperty', 'FloatProperty', 'DecimalProperty',
+ 'StringProperty', 'BooleanProperty',
+ 'DateProperty', 'DateTimeProperty', 'TimeProperty',
+ 'ObjectProperty', 'ListProperty', 'DictProperty', 'SetProperty',
+ 'JsonObject', 'JsonArray', 'AbstractDateProperty', 'JsonProperty',
+ 'DefaultProperty'
+]
diff --git a/meta/jsonobject/api.py b/meta/jsonobject/api.py
new file mode 100644
index 0000000000..8b9c4767c1
--- /dev/null
+++ b/meta/jsonobject/api.py
@@ -0,0 +1,53 @@
+from __future__ import absolute_import
+from .base import JsonObjectBase, _LimitedDictInterfaceMixin
+
+import six
+import decimal
+import datetime
+
+from . import properties
+import re
+
+re_date = re.compile(r'^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$')
+re_time = re.compile(
+ r'^([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3,6})?$')
+re_datetime = re.compile(
+ r'^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])'
+ r'(\D?([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3,6})?'
+ r'([zZ]|([\+-])([01]\d|2[0-3])\D?([0-5]\d)?)?)?$'
+)
+re_decimal = re.compile('^(\d+)\.(\d+)$')
+if six.PY3:
+ unicode = str
+ long = int
+
+
+class JsonObject(JsonObjectBase, _LimitedDictInterfaceMixin):
+ def __getstate__(self):
+ return self.to_json()
+
+ def __setstate__(self, dct):
+ self.__init__(dct)
+
+ class Meta(object):
+ properties = {
+ decimal.Decimal: properties.DecimalProperty,
+ datetime.datetime: properties.DateTimeProperty,
+ datetime.date: properties.DateProperty,
+ datetime.time: properties.TimeProperty,
+ str: properties.StringProperty,
+ unicode: properties.StringProperty,
+ bool: properties.BooleanProperty,
+ int: properties.IntegerProperty,
+ long: properties.IntegerProperty,
+ float: properties.FloatProperty,
+ list: properties.ListProperty,
+ dict: properties.DictProperty,
+ set: properties.SetProperty,
+ }
+ string_conversions = (
+ (re_date, datetime.date),
+ (re_time, datetime.time),
+ (re_datetime, datetime.datetime),
+ (re_decimal, decimal.Decimal),
+ )
diff --git a/meta/jsonobject/base.py b/meta/jsonobject/base.py
new file mode 100644
index 0000000000..90a5f44c6a
--- /dev/null
+++ b/meta/jsonobject/base.py
@@ -0,0 +1,394 @@
+from __future__ import absolute_import
+from collections import namedtuple, OrderedDict
+import copy
+import six
+import inspect
+from .exceptions import (
+ DeleteNotAllowed,
+ WrappingAttributeError,
+)
+from .base_properties import JsonProperty, DefaultProperty
+from .utils import check_type
+
+JsonObjectClassSettings = namedtuple('JsonObjectClassSettings', ['type_config'])
+
+CLASS_SETTINGS_ATTR = '_$_class_settings'
+
+
+def get_settings(cls):
+ return getattr(cls, CLASS_SETTINGS_ATTR,
+ JsonObjectClassSettings(type_config=TypeConfig()))
+
+
+def set_settings(cls, settings):
+ setattr(cls, CLASS_SETTINGS_ATTR, settings)
+
+
+class TypeConfig(object):
+ """
+ This class allows the user to configure dynamic
+ type handlers and string conversions for their JsonObject.
+
+ properties is a map from python types to JsonProperty subclasses
+ string_conversions is a list or tuple of (regex, python type)-tuples
+
+ This class is used to store the configuration but is not part of the API.
+ To configure:
+
+ class Foo(JsonObject):
+ # property definitions go here
+ # ...
+
+ class Meta(object):
+ update_properties = {
+ datetime.datetime: MySpecialDateTimeProperty
+ }
+ # this is already set by default
+ # but you can override with your own modifications
+ string_conversions = ((date_re, datetime.date),
+ (datetime_re, datetime.datetime),
+ (time_re, datetime.time),
+ (decimal_re, decimal.Decimal))
+
+ If you now do
+
+ foo = Foo()
+ foo.timestamp = datetime.datetime(1988, 7, 7, 11, 8, 0)
+
+ timestamp will be governed by a MySpecialDateTimeProperty
+ instead of the default.
+
+ """
+
+ def __init__(self, properties=None, string_conversions=None):
+ self._properties = properties if properties is not None else {}
+
+ self._string_conversions = (
+ OrderedDict(string_conversions) if string_conversions is not None
+ else OrderedDict()
+ )
+ # cache this
+ self.string_conversions = self._get_string_conversions()
+ self.properties = self._properties
+
+ def replace(self, properties=None, string_conversions=None):
+ return TypeConfig(
+ properties=(properties if properties is not None
+ else self._properties),
+ string_conversions=(string_conversions if string_conversions is not None
+ else self._string_conversions)
+ )
+
+ def updated(self, properties=None, string_conversions=None):
+ """
+ update properties and string_conversions with the paramenters
+ keeping all non-mentioned items the same as before
+ returns a new TypeConfig with these changes
+ (does not modify original)
+
+ """
+ _properties = self._properties.copy()
+ _string_conversions = self.string_conversions[:]
+ if properties:
+ _properties.update(properties)
+ if string_conversions:
+ _string_conversions.extend(string_conversions)
+ return TypeConfig(
+ properties=_properties,
+ string_conversions=_string_conversions,
+ )
+
+ def _get_string_conversions(self):
+ result = []
+ for pattern, conversion in self._string_conversions.items():
+ conversion = (
+ conversion if conversion not in self._properties
+ else self._properties[conversion](type_config=self).to_python
+ )
+ result.append((pattern, conversion))
+ return result
+
+
+META_ATTRS = ('properties', 'string_conversions', 'update_properties')
+
+
+class JsonObjectMeta(type):
+ class Meta(object):
+ pass
+
+ def __new__(mcs, name, bases, dct):
+ cls = super(JsonObjectMeta, mcs).__new__(mcs, name, bases, dct)
+
+ cls.__configure(**{key: value
+ for key, value in cls.Meta.__dict__.items()
+ if key in META_ATTRS})
+ cls_settings = get_settings(cls)
+
+ properties = {}
+ properties_by_name = {}
+ for key, value in dct.items():
+ if isinstance(value, JsonProperty):
+ properties[key] = value
+ elif key.startswith('_'):
+ continue
+ elif type(value) in cls_settings.type_config.properties:
+ property_ = cls_settings.type_config.properties[type(value)](default=value)
+ properties[key] = dct[key] = property_
+ setattr(cls, key, property_)
+
+ for key, property_ in properties.items():
+ property_.init_property(default_name=key,
+ type_config=cls_settings.type_config)
+ assert property_.name is not None, property_
+ assert property_.name not in properties_by_name, \
+ 'You can only have one property named {0}'.format(
+ property_.name)
+ properties_by_name[property_.name] = property_
+
+ for base in bases:
+ if getattr(base, '_properties_by_attr', None):
+ for key, value in base._properties_by_attr.items():
+ if key not in properties:
+ properties[key] = value
+ properties_by_name[value.name] = value
+
+ cls._properties_by_attr = properties
+ cls._properties_by_key = properties_by_name
+ return cls
+
+ def __configure(cls, properties=None, string_conversions=None,
+ update_properties=None):
+ super_settings = get_settings(super(cls, cls))
+ assert not properties or not update_properties, \
+ "{} {}".format(properties, update_properties)
+ type_config = super_settings.type_config
+ if update_properties is not None:
+ type_config = type_config.updated(properties=update_properties)
+ elif properties is not None:
+ type_config = type_config.replace(properties=properties)
+ if string_conversions is not None:
+ type_config = type_config.replace(
+ string_conversions=string_conversions)
+ set_settings(cls, super_settings._replace(type_config=type_config))
+ return cls
+
+
+class _JsonObjectPrivateInstanceVariables(object):
+
+ def __init__(self, dynamic_properties=None):
+ self.dynamic_properties = dynamic_properties or {}
+
+
+@six.add_metaclass(JsonObjectMeta)
+class JsonObjectBase(object):
+ _allow_dynamic_properties = False
+ _validate_required_lazily = False
+
+ _properties_by_attr = None
+ _properties_by_key = None
+
+ _string_conversions = ()
+
+ def __init__(self, _obj=None, **kwargs):
+ setattr(self, '_$', _JsonObjectPrivateInstanceVariables())
+
+ self._obj = check_type(_obj, dict,
+ 'JsonObject must wrap a dict or None')
+ self._wrapped = {}
+
+ for key, value in self._obj.items():
+ try:
+ self.set_raw_value(key, value)
+ except AttributeError:
+ raise WrappingAttributeError(
+ "can't set attribute corresponding to {key!r} "
+ "on a {cls} while wrapping {data!r}".format(
+ cls=self.__class__,
+ key=key,
+ data=_obj,
+ )
+ )
+
+ for attr, value in kwargs.items():
+ try:
+ setattr(self, attr, value)
+ except AttributeError:
+ raise WrappingAttributeError(
+ "can't set attribute {key!r} "
+ "on a {cls} while wrapping {data!r}".format(
+ cls=self.__class__,
+ key=attr,
+ data=_obj,
+ )
+ )
+
+ for key, value in self._properties_by_key.items():
+ if key not in self._obj:
+ try:
+ d = value.default()
+ except TypeError:
+ d = value.default(self)
+ self[key] = d
+
+ def set_raw_value(self, key, value):
+ wrapped = self.__wrap(key, value)
+ if key in self._properties_by_key:
+ self[key] = wrapped
+ else:
+ setattr(self, key, wrapped)
+
+ @classmethod
+ def properties(cls):
+ return cls._properties_by_attr.copy()
+
+ @property
+ def __dynamic_properties(self):
+ return getattr(self, '_$').dynamic_properties
+
+ @classmethod
+ def wrap(cls, obj):
+ self = cls(obj)
+ return self
+
+ def validate(self, required=True):
+ for key, value in self._wrapped.items():
+ self.__get_property(key).validate(value, required=required)
+
+ def to_json(self):
+ self.validate()
+ return copy.deepcopy(self._obj)
+
+ def __get_property(self, key):
+ try:
+ return self._properties_by_key[key]
+ except KeyError:
+ return DefaultProperty(type_config=get_settings(self).type_config)
+
+ def __wrap(self, key, value):
+ property_ = self.__get_property(key)
+
+ if value is None:
+ return None
+
+ return property_.wrap(value)
+
+ def __unwrap(self, key, value):
+ property_ = self.__get_property(key)
+ try:
+ property_.validate(
+ value,
+ required=not self._validate_required_lazily,
+ recursive=False,
+ )
+ except TypeError:
+ property_.validate(
+ value,
+ required=not self._validate_required_lazily,
+ )
+ if value is None:
+ return None, None
+
+ return property_.unwrap(value)
+
+ def __setitem__(self, key, value):
+ wrapped, unwrapped = self.__unwrap(key, value)
+ self._wrapped[key] = wrapped
+ if self.__get_property(key).exclude(unwrapped):
+ self._obj.pop(key, None)
+ else:
+ self._obj[key] = unwrapped
+ if key not in self._properties_by_key:
+ assert key not in self._properties_by_attr
+ self.__dynamic_properties[key] = wrapped
+ super(JsonObjectBase, self).__setattr__(key, wrapped)
+
+ def __is_dynamic_property(self, name):
+ return (
+ name not in self._properties_by_attr and
+ not name.startswith('_') and
+ not inspect.isdatadescriptor(getattr(self.__class__, name, None))
+ )
+
+ def __setattr__(self, name, value):
+ if self.__is_dynamic_property(name):
+ if self._allow_dynamic_properties:
+ self[name] = value
+ else:
+ raise AttributeError(
+ "{0!r} is not defined in schema "
+ "(not a valid property)".format(name)
+ )
+ else:
+ super(JsonObjectBase, self).__setattr__(name, value)
+
+ def __delitem__(self, key):
+ if key in self._properties_by_key:
+ raise DeleteNotAllowed(key)
+ else:
+ if not self.__is_dynamic_property(key):
+ raise KeyError(key)
+ del self._obj[key]
+ del self._wrapped[key]
+ del self.__dynamic_properties[key]
+ super(JsonObjectBase, self).__delattr__(key)
+
+ def __delattr__(self, name):
+ if name in self._properties_by_attr:
+ raise DeleteNotAllowed(name)
+ elif self.__is_dynamic_property(name):
+ del self[name]
+ else:
+ super(JsonObjectBase, self).__delattr__(name)
+
+ def __repr__(self):
+ name = self.__class__.__name__
+ predefined_properties = self._properties_by_attr.keys()
+ predefined_property_keys = set(self._properties_by_attr[p].name
+ for p in predefined_properties)
+ dynamic_properties = (set(self._wrapped.keys())
+ - predefined_property_keys)
+ properties = sorted(predefined_properties) + sorted(dynamic_properties)
+ return u'{name}({keyword_args})'.format(
+ name=name,
+ keyword_args=', '.join('{key}={value!r}'.format(
+ key=key,
+ value=getattr(self, key)
+ ) for key in properties),
+ )
+
+
+class _LimitedDictInterfaceMixin(object):
+ """
+ mindlessly farms selected dict methods out to an internal dict
+
+ really only a separate class from JsonObject
+ to keep this mindlessness separate from the methods
+ that need to be more carefully understood
+
+ """
+ _wrapped = None
+
+ def keys(self):
+ return self._wrapped.keys()
+
+ def items(self):
+ return self._wrapped.items()
+
+ def iteritems(self):
+ return self._wrapped.iteritems()
+
+ def __contains__(self, item):
+ return item in self._wrapped
+
+ def __getitem__(self, item):
+ return self._wrapped[item]
+
+ def __iter__(self):
+ return iter(self._wrapped)
+
+ def __len__(self):
+ return len(self._wrapped)
+
+
+def get_dynamic_properties(obj):
+ return getattr(obj, '_$').dynamic_properties.copy()
diff --git a/meta/jsonobject/base_properties.py b/meta/jsonobject/base_properties.py
new file mode 100644
index 0000000000..b43b1d44e2
--- /dev/null
+++ b/meta/jsonobject/base_properties.py
@@ -0,0 +1,320 @@
+from __future__ import absolute_import
+import six
+import inspect
+from .exceptions import BadValueError
+
+function_name = None
+if six.PY3:
+ def function_name(f):
+ return f.__name__
+else:
+ def function_name(f):
+ return f.func_name
+
+
+class JsonProperty(object):
+ default = None
+ type_config = None
+
+ def __init__(self, default=Ellipsis, name=None, choices=None,
+ required=False, exclude_if_none=False, validators=None,
+ verbose_name=None, type_config=None):
+ validators = validators or ()
+ self.name = name
+ if default is Ellipsis:
+ default = self.default
+ if callable(default):
+ self.default = default
+ else:
+ self.default = lambda: default
+ self.choices = choices
+ self.choice_keys = []
+ if choices:
+ for choice in choices:
+ if isinstance(choice, tuple):
+ choice, _ = choice
+ self.choice_keys.append(choice)
+ self.required = required
+ self.exclude_if_none = exclude_if_none
+ self._validators = validators
+ self.verbose_name = verbose_name
+ if type_config:
+ self.type_config = type_config
+
+ def init_property(self, default_name, type_config):
+ self.name = self.name or default_name
+ self.type_config = self.type_config or type_config
+
+ def wrap(self, obj):
+ raise NotImplementedError()
+
+ def unwrap(self, obj):
+ """
+ must return tuple of (wrapped, unwrapped)
+
+ If obj is already a fully wrapped object,
+ it must be returned as the first element.
+
+ For an example where the first element is relevant see ListProperty
+
+ """
+ raise NotImplementedError()
+
+ def to_json(self, value):
+ _, unwrapped = self.unwrap(value)
+ return unwrapped
+
+ def to_python(self, value):
+ return self.wrap(value)
+
+ def __get__(self, instance, owner):
+ if instance:
+ assert self.name in instance
+ return instance[self.name]
+ else:
+ return self
+
+ def __set__(self, instance, value):
+ instance[self.name] = value
+
+ def __call__(self, method):
+ """
+ use a property as a decorator to set its default value
+
+ class Document(JsonObject):
+ @StringProperty()
+ def doc_type(self):
+ return self.__class__.__name__
+ """
+ assert self.default() is None
+ self.default = method
+ self.name = self.name or function_name(method)
+ return self
+
+ def exclude(self, value):
+ return self.exclude_if_none and value == None
+
+ def empty(self, value):
+ return value is None
+
+ def validate(self, value, required=True, recursive=True):
+ if (self.choice_keys and value not in self.choice_keys
+ and value is not None):
+ raise BadValueError(
+ '{0!r} not in choices: {1!r}'.format(value, self.choice_keys)
+ )
+
+ if not self.empty(value):
+ self._custom_validate(value)
+ elif required and self.required:
+ raise BadValueError(
+ 'Property {0} is required.'.format(self.name)
+ )
+ if recursive and hasattr(value, 'validate'):
+ value.validate(required=required)
+
+ def _custom_validate(self, value):
+ if self._validators:
+ if hasattr(self._validators, '__iter__'):
+ for validator in self._validators:
+ validator(value)
+ else:
+ self._validators(value)
+
+
+class JsonContainerProperty(JsonProperty):
+ _type = default = None
+ container_class = None
+
+ def __init__(self, item_type=None, **kwargs):
+ self._item_type_deferred = item_type
+ super(JsonContainerProperty, self).__init__(**kwargs)
+
+ def init_property(self, **kwargs):
+ super(JsonContainerProperty, self).init_property(**kwargs)
+ if not inspect.isfunction(self._item_type_deferred):
+ # trigger validation
+ self.item_type
+
+ def set_item_type(self, item_type):
+ from meta.jsonobject.base import JsonObjectMeta
+ if hasattr(item_type, '_type'):
+ item_type = item_type._type
+ if isinstance(item_type, tuple):
+ # this is for the case where item_type = (int, long)
+ item_type = item_type[0]
+ allowed_types = set(self.type_config.properties.keys())
+ if isinstance(item_type, JsonObjectMeta) \
+ or not item_type or item_type in allowed_types:
+ self._item_type = item_type
+ else:
+ raise ValueError("item_type {0!r} not in {1!r}".format(
+ item_type,
+ allowed_types,
+ ))
+
+ @property
+ def item_type(self):
+ if hasattr(self, '_item_type_deferred'):
+ if inspect.isfunction(self._item_type_deferred):
+ self.set_item_type(self._item_type_deferred())
+ else:
+ self.set_item_type(self._item_type_deferred)
+ del self._item_type_deferred
+ return self._item_type
+
+ def empty(self, value):
+ return not value
+
+ def wrap(self, obj):
+ wrapper = self.type_to_property(self.item_type) if self.item_type else None
+ return self.container_class(obj, wrapper=wrapper,
+ type_config=self.type_config)
+
+ def type_to_property(self, item_type):
+ map_types_properties = self.type_config.properties
+ from .properties import ObjectProperty
+ from .base import JsonObjectBase
+ if issubclass(item_type, JsonObjectBase):
+ return ObjectProperty(item_type, type_config=self.type_config)
+ elif item_type in map_types_properties:
+ return map_types_properties[item_type](type_config=self.type_config)
+ else:
+ for key, value in map_types_properties.items():
+ if issubclass(item_type, key):
+ return value(type_config=self.type_config)
+ raise TypeError('Type {0} not recognized'.format(item_type))
+
+ def unwrap(self, obj):
+ if not isinstance(obj, self._type):
+ raise BadValueError(
+ '{0!r} is not an instance of {1!r}'.format(
+ obj, self._type.__name__)
+ )
+ if isinstance(obj, self.container_class):
+ return obj, obj._obj
+ else:
+ wrapped = self.wrap(self._type())
+ self._update(wrapped, obj)
+ return self.unwrap(wrapped)
+
+ def _update(self, container, extension):
+ raise NotImplementedError()
+
+
+class DefaultProperty(JsonProperty):
+
+ def wrap(self, obj):
+ assert self.type_config.string_conversions is not None
+ value = self.value_to_python(obj)
+ property_ = self.value_to_property(value)
+
+ if property_:
+ return property_.wrap(obj)
+
+ def unwrap(self, obj):
+ property_ = self.value_to_property(obj)
+ if property_:
+ return property_.unwrap(obj)
+ else:
+ return obj, None
+
+ def value_to_property(self, value):
+ map_types_properties = self.type_config.properties
+ if value is None:
+ return None
+ elif type(value) in map_types_properties:
+ return map_types_properties[type(value)](
+ type_config=self.type_config)
+ else:
+ for value_type, prop_class in map_types_properties.items():
+ if isinstance(value, value_type):
+ return prop_class(type_config=self.type_config)
+ else:
+ raise BadValueError(
+ 'value {0!r} not in allowed types: {1!r}'.format(
+ value, map_types_properties.keys())
+ )
+
+ def value_to_python(self, value):
+ """
+ convert encoded string values to the proper python type
+
+ ex:
+ >>> DefaultProperty().value_to_python('2013-10-09T10:05:51Z')
+ datetime.datetime(2013, 10, 9, 10, 5, 51)
+
+ other values will be passed through unmodified
+ Note: containers' items are NOT recursively converted
+
+ """
+ if isinstance(value, six.string_types):
+ convert = None
+ for pattern, _convert in self.type_config.string_conversions:
+ if pattern.match(value):
+ convert = _convert
+ break
+
+ if convert is not None:
+ try:
+ # sometimes regex fail so return value
+ value = convert(value)
+ except Exception:
+ pass
+ return value
+
+
+class AssertTypeProperty(JsonProperty):
+ _type = None
+
+ def assert_type(self, obj):
+ if not isinstance(obj, self._type):
+ raise BadValueError(
+ '{0!r} not of type {1!r}'.format(obj, self._type)
+ )
+
+ def selective_coerce(self, obj):
+ return obj
+
+ def wrap(self, obj):
+ obj = self.selective_coerce(obj)
+ self.assert_type(obj)
+ return obj
+
+ def unwrap(self, obj):
+ obj = self.selective_coerce(obj)
+ self.assert_type(obj)
+ return obj, obj
+
+
+class AbstractDateProperty(JsonProperty):
+ _type = None
+
+ def __init__(self, exact=False, *args, **kwargs):
+ super(AbstractDateProperty, self).__init__(*args, **kwargs)
+ self.exact = exact
+
+ def wrap(self, obj):
+ try:
+ if not isinstance(obj, six.string_types):
+ raise ValueError()
+ return self._wrap(obj)
+ except ValueError:
+ raise BadValueError('{0!r} is not a {1}-formatted string'.format(
+ obj,
+ self._type.__name__,
+ ))
+
+ def unwrap(self, obj):
+ if not isinstance(obj, self._type):
+ raise BadValueError('{0!r} is not a {1} object'.format(
+ obj,
+ self._type.__name__,
+ ))
+ return self._unwrap(obj)
+
+ def _wrap(self, obj):
+ raise NotImplementedError()
+
+ def _unwrap(self, obj):
+ raise NotImplementedError()
diff --git a/meta/jsonobject/containers.py b/meta/jsonobject/containers.py
new file mode 100644
index 0000000000..1150ab9d23
--- /dev/null
+++ b/meta/jsonobject/containers.py
@@ -0,0 +1,252 @@
+from __future__ import absolute_import
+from .base_properties import DefaultProperty
+from .utils import check_type, SimpleDict
+import copy
+
+
+class JsonArray(list):
+ def __init__(self, _obj=None, wrapper=None, type_config=None):
+ super(JsonArray, self).__init__()
+ self._obj = check_type(_obj, list,
+ 'JsonArray must wrap a list or None')
+
+ assert type_config is not None
+ self._type_config = type_config
+ self._wrapper = (
+ wrapper or
+ DefaultProperty(type_config=self._type_config)
+ )
+ for item in self._obj:
+ super(JsonArray, self).append(self._wrapper.wrap(item))
+
+ def validate(self, required=True):
+ for obj in self:
+ self._wrapper.validate(obj, required=required)
+
+ def to_json(self):
+ self.validate()
+ return copy.deepcopy(self._obj)
+
+ def append(self, wrapped):
+ wrapped, unwrapped = self._wrapper.unwrap(wrapped)
+ self._obj.append(unwrapped)
+ super(JsonArray, self).append(wrapped)
+
+ def __delitem__(self, i):
+ super(JsonArray, self).__delitem__(i)
+ del self._obj[i]
+
+ def __setitem__(self, i, wrapped):
+ wrapped, unwrapped = self._wrapper.unwrap(wrapped)
+ self._obj[i] = unwrapped
+ super(JsonArray, self).__setitem__(i, wrapped)
+
+ def extend(self, wrapped_list):
+ if wrapped_list:
+ wrapped_list, unwrapped_list = zip(
+ *map(self._wrapper.unwrap, wrapped_list)
+ )
+ else:
+ unwrapped_list = []
+ self._obj.extend(unwrapped_list)
+ super(JsonArray, self).extend(wrapped_list)
+
+ def insert(self, index, wrapped):
+ wrapped, unwrapped = self._wrapper.unwrap(wrapped)
+ self._obj.insert(index, unwrapped)
+ super(JsonArray, self).insert(index, wrapped)
+
+ def remove(self, value):
+ i = self.index(value)
+ super(JsonArray, self).remove(value)
+ self._obj.pop(i)
+
+ def pop(self, index=-1):
+ self._obj.pop(index)
+ return super(JsonArray, self).pop(index)
+
+ def sort(self, cmp=None, key=None, reverse=False):
+ zipped = zip(self, self._obj)
+ if key:
+ new_key = lambda pair: key(pair[0])
+ zipped.sort(key=new_key, reverse=reverse)
+ elif cmp:
+ new_cmp = lambda pair1, pair2: cmp(pair1[0], pair2[0])
+ zipped.sort(cmp=new_cmp, reverse=reverse)
+ else:
+ zipped.sort(reverse=reverse)
+
+ wrapped_list, unwrapped_list = zip(*zipped)
+ while self:
+ self.pop()
+ super(JsonArray, self).extend(wrapped_list)
+ self._obj.extend(unwrapped_list)
+
+ def reverse(self):
+ self._obj.reverse()
+ super(JsonArray, self).reverse()
+
+ def __fix_slice(self, i, j):
+ length = len(self)
+ if j < 0:
+ j += length
+ if i < 0:
+ i += length
+ if i > length:
+ i = length
+ if j > length:
+ j = length
+ return i, j
+
+ def __setslice__(self, i, j, sequence):
+ i, j = self.__fix_slice(i, j)
+ for _ in range(j - i):
+ self.pop(i)
+ for k, wrapped in enumerate(sequence):
+ self.insert(i + k, wrapped)
+
+ def __delslice__(self, i, j):
+ i, j = self.__fix_slice(i, j)
+ for _ in range(j - i):
+ self.pop(i)
+
+
+class JsonDict(SimpleDict):
+
+ def __init__(self, _obj=None, wrapper=None, type_config=None):
+ super(JsonDict, self).__init__()
+ self._obj = check_type(_obj, dict, 'JsonDict must wrap a dict or None')
+ assert type_config is not None
+ self._type_config = type_config
+ self._wrapper = (
+ wrapper or
+ DefaultProperty(type_config=self._type_config)
+ )
+ for key, value in self._obj.items():
+ self[key] = self.__wrap(key, value)
+
+ def validate(self, required=True):
+ for obj in self.values():
+ self._wrapper.validate(obj, required=required)
+
+ def __wrap(self, key, unwrapped):
+ return self._wrapper.wrap(unwrapped)
+
+ def __unwrap(self, key, wrapped):
+ return self._wrapper.unwrap(wrapped)
+
+ def __setitem__(self, key, value):
+ if isinstance(key, int):
+ key = str(key)
+
+ wrapped, unwrapped = self.__unwrap(key, value)
+ self._obj[key] = unwrapped
+ super(JsonDict, self).__setitem__(key, wrapped)
+
+ def __delitem__(self, key):
+ del self._obj[key]
+ super(JsonDict, self).__delitem__(key)
+
+ def __getitem__(self, key):
+ if isinstance(key, int):
+ key = str(key)
+ return super(JsonDict, self).__getitem__(key)
+
+
+class JsonSet(set):
+ def __init__(self, _obj=None, wrapper=None, type_config=None):
+ super(JsonSet, self).__init__()
+ if isinstance(_obj, set):
+ _obj = list(_obj)
+ self._obj = check_type(_obj, list, 'JsonSet must wrap a list or None')
+ assert type_config is not None
+ self._type_config = type_config
+ self._wrapper = (
+ wrapper or
+ DefaultProperty(type_config=self._type_config)
+ )
+ for item in self._obj:
+ super(JsonSet, self).add(self._wrapper.wrap(item))
+
+ def validate(self, required=True):
+ for obj in self:
+ self._wrapper.validate(obj, required=required)
+
+ def add(self, wrapped):
+ wrapped, unwrapped = self._wrapper.unwrap(wrapped)
+ if wrapped not in self:
+ self._obj.append(unwrapped)
+ super(JsonSet, self).add(wrapped)
+
+ def remove(self, wrapped):
+ wrapped, unwrapped = self._wrapper.unwrap(wrapped)
+ if wrapped in self:
+ self._obj.remove(unwrapped)
+ super(JsonSet, self).remove(wrapped)
+ else:
+ raise KeyError(wrapped)
+
+ def discard(self, wrapped):
+ try:
+ self.remove(wrapped)
+ except KeyError:
+ pass
+
+ def pop(self):
+ # get first item
+ for wrapped in self:
+ break
+ else:
+ raise KeyError()
+ wrapped_, unwrapped = self._wrapper.unwrap(wrapped)
+ assert wrapped is wrapped_
+ self.remove(unwrapped)
+ return wrapped
+
+ def clear(self):
+ while self:
+ self.pop()
+
+ def __ior__(self, other):
+ for wrapped in other:
+ self.add(wrapped)
+ return self
+
+ def update(self, *args):
+ for wrapped_list in args:
+ self |= set(wrapped_list)
+
+ union_update = update
+
+ def __iand__(self, other):
+ for wrapped in list(self):
+ if wrapped not in other:
+ self.remove(wrapped)
+ return self
+
+ def intersection_update(self, *args):
+ for wrapped_list in args:
+ self &= set(wrapped_list)
+
+ def __isub__(self, other):
+ for wrapped in list(self):
+ if wrapped in other:
+ self.remove(wrapped)
+ return self
+
+ def difference_update(self, *args):
+ for wrapped_list in args:
+ self -= set(wrapped_list)
+
+ def __ixor__(self, other):
+ removed = set()
+ for wrapped in list(self):
+ if wrapped in other:
+ self.remove(wrapped)
+ removed.add(wrapped)
+ self.update(other - removed)
+ return self
+
+ def symmetric_difference_update(self, *args):
+ for wrapped_list in args:
+ self ^= set(wrapped_list)
diff --git a/meta/jsonobject/exceptions.py b/meta/jsonobject/exceptions.py
new file mode 100644
index 0000000000..a42022e120
--- /dev/null
+++ b/meta/jsonobject/exceptions.py
@@ -0,0 +1,10 @@
+class DeleteNotAllowed(Exception):
+ pass
+
+
+class BadValueError(Exception):
+ """raised when a value can't be validated or is required"""
+
+
+class WrappingAttributeError(AttributeError):
+ pass
diff --git a/meta/jsonobject/properties.py b/meta/jsonobject/properties.py
new file mode 100644
index 0000000000..05bba86da9
--- /dev/null
+++ b/meta/jsonobject/properties.py
@@ -0,0 +1,155 @@
+# DateTimeProperty, DateProperty, and TimeProperty
+# include code copied from couchdbkit
+from __future__ import absolute_import
+import sys
+import datetime
+import time
+import decimal
+from .base_properties import (
+ AbstractDateProperty,
+ AssertTypeProperty,
+ JsonContainerProperty,
+ JsonProperty,
+ DefaultProperty,
+)
+from .containers import JsonArray, JsonDict, JsonSet
+
+if sys.version > '3':
+ unicode = str
+ long = int
+
+
+class StringProperty(AssertTypeProperty):
+ _type = (unicode, str)
+
+ def selective_coerce(self, obj):
+ if isinstance(obj, str):
+ obj = unicode(obj)
+ return obj
+
+
+class BooleanProperty(AssertTypeProperty):
+ _type = bool
+
+
+class IntegerProperty(AssertTypeProperty):
+ _type = (int, long)
+
+
+class FloatProperty(AssertTypeProperty):
+ _type = float
+
+ def selective_coerce(self, obj):
+ if isinstance(obj, (int, long)):
+ obj = float(obj)
+ return obj
+
+
+class DecimalProperty(JsonProperty):
+
+ def wrap(self, obj):
+ return decimal.Decimal(obj)
+
+ def unwrap(self, obj):
+ if isinstance(obj, (int, long)):
+ obj = decimal.Decimal(obj)
+ elif isinstance(obj, float):
+ # python 2.6 doesn't allow a float to Decimal
+ obj = decimal.Decimal(unicode(obj))
+ assert isinstance(obj, decimal.Decimal)
+ return obj, unicode(obj)
+
+
+class DateProperty(AbstractDateProperty):
+ _type = datetime.date
+
+ def _wrap(self, value):
+ fmt = '%Y-%m-%d'
+ try:
+ return datetime.date(*time.strptime(value, fmt)[:3])
+ except ValueError as e:
+ raise ValueError('Invalid ISO date {0!r} [{1}]'.format(value, e))
+
+ def _unwrap(self, value):
+ return value, value.isoformat()
+
+
+class DateTimeProperty(AbstractDateProperty):
+ _type = datetime.datetime
+
+ def _wrap(self, value):
+ if not self.exact:
+ value = value.split('.', 1)[0] # strip out microseconds
+ value = value[0:19] # remove timezone
+ fmt = '%Y-%m-%dT%H:%M:%S'
+ else:
+ fmt = '%Y-%m-%dT%H:%M:%S.%fZ'
+ try:
+ return datetime.datetime.strptime(value, fmt)
+ except ValueError as e:
+ raise ValueError(
+ 'Invalid ISO date/time {0!r} [{1}]'.format(value, e))
+
+ def _unwrap(self, value):
+ if not self.exact:
+ value = value.replace(microsecond=0)
+ padding = ''
+ else:
+ padding = '' if value.microsecond else '.000000'
+ return value, value.isoformat() + padding + 'Z'
+
+
+class TimeProperty(AbstractDateProperty):
+ _type = datetime.time
+
+ def _wrap(self, value):
+ if not self.exact:
+ value = value.split('.', 1)[0] # strip out microseconds
+ fmt = '%H:%M:%S'
+ else:
+ fmt = '%H:%M:%S.%f'
+ try:
+ return datetime.time(*time.strptime(value, fmt)[3:6])
+ except ValueError as e:
+ raise ValueError('Invalid ISO time {0!r} [{1}]'.format(value, e))
+
+ def _unwrap(self, value):
+ if not self.exact:
+ value = value.replace(microsecond=0)
+ return value, value.isoformat()
+
+
+class ObjectProperty(JsonContainerProperty):
+ default = lambda self: self.item_type()
+
+ def wrap(self, obj, string_conversions=None):
+ return self.item_type.wrap(obj)
+
+ def unwrap(self, obj):
+ assert isinstance(obj, self.item_type), \
+ '{0} is not an instance of {1}'.format(obj, self.item_type)
+ return obj, obj._obj
+
+
+class ListProperty(JsonContainerProperty):
+ _type = default = list
+ container_class = JsonArray
+
+ def _update(self, container, extension):
+ container.extend(extension)
+
+
+class DictProperty(JsonContainerProperty):
+ _type = default = dict
+ container_class = JsonDict
+
+ def _update(self, container, extension):
+ container.update(extension)
+
+
+class SetProperty(JsonContainerProperty):
+ _type = default = set
+ container_class = JsonSet
+
+ def _update(self, container, extension):
+ container.update(extension)
diff --git a/meta/jsonobject/utils.py b/meta/jsonobject/utils.py
new file mode 100644
index 0000000000..9ee8569801
--- /dev/null
+++ b/meta/jsonobject/utils.py
@@ -0,0 +1,57 @@
+from __future__ import absolute_import
+from .exceptions import BadValueError
+
+
+def check_type(obj, item_type, message):
+ if obj is None:
+ return item_type()
+ elif not isinstance(obj, item_type):
+ raise BadValueError('{}. Found object of type: {}'.format(message, type(obj)))
+ else:
+ return obj
+
+
+class SimpleDict(dict):
+ """
+ Re-implements destructive methods of dict
+ to use only setitem and getitem and delitem
+ """
+
+ def update(self, E=None, **F):
+ for dct in (E, F):
+ if dct:
+ for key, value in dct.items():
+ self[key] = value
+
+ def clear(self):
+ for key in list(self.keys()):
+ del self[key]
+
+ def pop(self, key, *args):
+ if len(args) > 1:
+ raise TypeError('pop expected at most 2 arguments, got 3')
+ try:
+ val = self[key]
+ del self[key]
+ return val
+ except KeyError:
+ try:
+ return args[0]
+ except IndexError:
+ raise KeyError(key)
+
+ def popitem(self):
+ try:
+ arbitrary_key = list(self.keys())[0]
+ except IndexError:
+ raise KeyError('popitem(): dictionary is empty')
+ val = self[arbitrary_key]
+ del self[arbitrary_key]
+ return (arbitrary_key, val)
+
+ def setdefault(self, key, default=None):
+ try:
+ return self[key]
+ except KeyError:
+ self[key] = default
+ return default
diff --git a/meta/liteloaderutil.py b/meta/liteloaderutil.py
new file mode 100644
index 0000000000..dce6cb19d4
--- /dev/null
+++ b/meta/liteloaderutil.py
@@ -0,0 +1,118 @@
+from .metautil import *
+
+'''
+ "repo":{
+ "stream":"RELEASE",
+ "type":"m2",
+ "url":"http:\/\/dl.liteloader.com\/repo\/",
+ "classifier":""
+ },
+'''
+
+
+class LiteloaderRepo(JsonObject):
+ stream = StringProperty(required=True)
+ type = StringProperty(required=True)
+ url = StringProperty(required=True)
+ classifier = StringProperty(required=True)
+
+
+'''
+ "53639d52340479ccf206a04f5e16606f":{
+ "tweakClass":"com.mumfrey.liteloader.launch.LiteLoaderTweaker",
+ "libraries":[
+ {
+ "name":"net.minecraft:launchwrapper:1.5"
+ },
+ {
+ "name":"net.sf.jopt-simple:jopt-simple:4.5"
+ },
+ {
+ "name":"org.ow2.asm:asm-all:4.1"
+ }
+ ],
+ "stream":"RELEASE",
+ "file":"liteloader-1.5.2_01.jar",
+ "version":"1.5.2_01",
+ "md5":"53639d52340479ccf206a04f5e16606f",
+ "timestamp":"1367366420"
+ },
+'''
+
+
+class LiteloaderArtefact(JsonObject):
+ tweakClass = StringProperty(required=True)
+ libraries = ListProperty(PolyMCLibrary, required=True)
+ stream = StringProperty(required=True)
+ file = StringProperty(required=True)
+ version = StringProperty(required=True)
+ build = StringProperty(default=None, exclude_if_none=True)
+ md5 = StringProperty(required=True)
+ timestamp = StringProperty(required=True)
+ srcJar = StringProperty(default=None, exclude_if_none=True)
+ mcpJar = StringProperty(default=None, exclude_if_none=True)
+
+
+class LiteloaderDev(JsonObject):
+ fgVersion = StringProperty(default=None, exclude_if_none=True)
+ mappings = StringProperty(required=None, exclude_if_none=True)
+ mcp = StringProperty(default=None, exclude_if_none=True)
+
+
+class LiteloaderArtefacts(JsonObject):
+ liteloader = DictProperty(LiteloaderArtefact, name="com.mumfrey:liteloader", required=True)
+
+
+class LiteloaderSnapshot(LiteloaderArtefact):
+ lastSuccessfulBuild = IntegerProperty()
+
+
+class LiteloaderSnapshots(JsonObject):
+ libraries = ListProperty(PolyMCLibrary, required=True)
+ liteloader = DictProperty(LiteloaderSnapshot, name="com.mumfrey:liteloader", required=True)
+
+
+'''
+ "1.10.2":{
+ "dev": { ... },
+ "repo":{ ... },
+ "artefacts":{
+ "com.mumfrey:liteloader":{ },
+ ...
+ },
+ "snapshots":{
+ ...
+ }
+'''
+
+
+class LiteloaderEntry(JsonObject):
+ dev = ObjectProperty(LiteloaderDev, default=None, exclude_if_none=True)
+ repo = ObjectProperty(LiteloaderRepo, required=True)
+ artefacts = ObjectProperty(LiteloaderArtefacts, default=None, exclude_if_none=True)
+ snapshots = ObjectProperty(LiteloaderSnapshots, default=None, exclude_if_none=True)
+
+
+'''
+ "meta":{
+ "description":"LiteLoader is a lightweight mod bootstrap designed to provide basic loader functionality for mods which don't need to modify game mechanics.",
+ "authors":"Mumfrey",
+ "url":"http:\/\/dl.liteloader.com",
+ "updated":"2017-02-22T11:34:07+00:00",
+ "updatedTime":1487763247
+ },
+'''
+
+
+class LiteloaderMeta(JsonObject):
+ description = StringProperty(required=True)
+ authors = StringProperty(required=True)
+ url = StringProperty(required=True)
+ updated = ISOTimestampProperty(required=True)
+ updatedTime = IntegerProperty(required=True)
+
+
+# The raw Forge version index
+class LiteloaderIndex(JsonObject):
+ meta = ObjectProperty(LiteloaderMeta, required=True)
+ versions = DictProperty(LiteloaderEntry)
diff --git a/meta/metautil.py b/meta/metautil.py
new file mode 100644
index 0000000000..e417bd6e65
--- /dev/null
+++ b/meta/metautil.py
@@ -0,0 +1,477 @@
+import datetime
+import json
+import os
+
+import iso8601
+from .jsonobject import *
+
+PMC_DIR = os.environ["PMC_DIR"]
+
+
+class ISOTimestampProperty(AbstractDateProperty):
+ _type = datetime.datetime
+
+ def _wrap(self, value):
+ try:
+ return iso8601.parse_date(value)
+ except ValueError as e:
+ raise ValueError(
+ 'Invalid ISO date/time {0!r} [{1}]'.format(value, e))
+
+ def _unwrap(self, value):
+ return value, value.isoformat()
+
+
+class GradleSpecifier:
+ '''
+ A gradle specifier - a maven coordinate. Like one of these:
+ "org.lwjgl.lwjgl:lwjgl:2.9.0"
+ "net.java.jinput:jinput:2.0.5"
+ "net.minecraft:launchwrapper:1.5"
+ '''
+
+ def __init__(self, name):
+ atSplit = name.split('@')
+
+ components = atSplit[0].split(':')
+ self.group = components[0]
+ self.artifact = components[1]
+ self.version = components[2]
+
+ self.extension = 'jar'
+ if len(atSplit) == 2:
+ self.extension = atSplit[1]
+
+ if len(components) == 4:
+ self.classifier = components[3]
+ else:
+ self.classifier = None
+
+ def toString(self):
+ extensionStr = ''
+ if self.extension != 'jar':
+ extensionStr = "@%s" % self.extension
+ if self.classifier:
+ return "%s:%s:%s:%s%s" % (self.group, self.artifact, self.version, self.classifier, extensionStr)
+ else:
+ return "%s:%s:%s%s" % (self.group, self.artifact, self.version, extensionStr)
+
+ def getFilename(self):
+ if self.classifier:
+ return "%s-%s-%s.%s" % (self.artifact, self.version, self.classifier, self.extension)
+ else:
+ return "%s-%s.%s" % (self.artifact, self.version, self.extension)
+
+ def getBase(self):
+ return "%s/%s/%s/" % (self.group.replace('.', '/'), self.artifact, self.version)
+
+ def getPath(self):
+ return self.getBase() + self.getFilename()
+
+ def __repr__(self):
+ return "GradleSpecifier('" + self.toString() + "')"
+
+ def isLwjgl(self):
+ return self.group in ("org.lwjgl", "org.lwjgl.lwjgl", "net.java.jinput", "net.java.jutils")
+
+ def isLog4j(self):
+ return self.group == "org.apache.logging.log4j"
+
+ def __lt__(self, other):
+ return self.toString() < other.toString()
+
+ def __eq__(self, other):
+ return self.group == other.group and self.artifact == other.artifact and self.version == other.version and self.classifier == other.classifier
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __hash__(self):
+ return self.toString().__hash__()
+
+
+class GradleSpecifierProperty(JsonProperty):
+ def wrap(self, value):
+ return GradleSpecifier(value)
+
+ def unwrap(self, value):
+ return value, value.toString()
+
+
+'''
+Mojang index files look like this:
+{
+ "latest": {
+ "release": "1.11.2",
+ "snapshot": "17w06a"
+ },
+ "versions": [
+ ...
+ {
+ "id": "17w06a",
+ "releaseTime": "2017-02-08T13:16:29+00:00",
+ "time": "2017-02-08T13:17:20+00:00",
+ "type": "snapshot",
+ "url": "https://launchermeta.mojang.com/mc/game/7db0c61afa278d016cf1dae2fba0146edfbf2f8e/17w06a.json"
+ },
+ ...
+ ]
+}
+'''
+
+
+class MojangIndexEntry(JsonObject):
+ id = StringProperty()
+ releaseTime = ISOTimestampProperty()
+ time = ISOTimestampProperty()
+ type = StringProperty()
+ url = StringProperty()
+ sha1 = StringProperty(exclude_if_none=True, default=None)
+ complianceLevel = IntegerProperty(exclude_if_none=True, default=None)
+
+
+class MojangIndex(JsonObject):
+ latest = DictProperty(StringProperty)
+ versions = ListProperty(MojangIndexEntry)
+
+
+class MojangIndexWrap:
+ def __init__(self, json):
+ self.index = MojangIndex.wrap(json)
+ self.latest = self.index.latest
+ versionsDict = {}
+ for version in self.index.versions:
+ versionsDict[version.id] = version
+ self.versions = versionsDict
+
+
+class MojangArtifactBase(JsonObject):
+ sha1 = StringProperty(exclude_if_none=True, default=None)
+ size = IntegerProperty(exclude_if_none=True, default=None)
+ url = StringProperty()
+
+
+class MojangArtifact(MojangArtifactBase):
+ path = StringProperty(exclude_if_none=True, default=None)
+
+
+class MojangAssets(MojangArtifactBase):
+ id = StringProperty()
+ totalSize = IntegerProperty()
+
+
+class MojangLibraryDownloads(JsonObject):
+ artifact = ObjectProperty(MojangArtifact, exclude_if_none=True, default=None)
+ classifiers = DictProperty(MojangArtifact, exclude_if_none=True, default=None)
+
+
+class MojangLibraryExtractRules(JsonObject):
+ exclude = ListProperty(StringProperty)
+
+
+'''
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+'''
+
+
+class OSRule(JsonObject):
+ name = StringProperty(choices=["osx", "linux", "windows"], required=True)
+ version = StringProperty(exclude_if_none=True, default=None)
+
+
+class MojangRule(JsonObject):
+ action = StringProperty(choices=["allow", "disallow"], required=True)
+ os = ObjectProperty(OSRule, exclude_if_none=True, default=None)
+
+
+class MojangLibrary(JsonObject):
+ extract = ObjectProperty(MojangLibraryExtractRules, exclude_if_none=True, default=None)
+ name = GradleSpecifierProperty(required=True)
+ downloads = ObjectProperty(MojangLibraryDownloads, exclude_if_none=True, default=None)
+ natives = DictProperty(StringProperty, exclude_if_none=True, default=None)
+ rules = ListProperty(MojangRule, exclude_if_none=True, default=None)
+
+
+class MojangLoggingArtifact(MojangArtifactBase):
+ id = StringProperty()
+
+
+class MojangLogging(JsonObject):
+ file = ObjectProperty(MojangLoggingArtifact, required=True)
+ argument = StringProperty(required=True)
+ type = StringProperty(required=True, choices=["log4j2-xml"])
+
+
+class MojangArguments(JsonObject):
+ game = ListProperty(exclude_if_none=True, default=None)
+ jvm = ListProperty(exclude_if_none=True, default=None)
+
+
+class JavaVersion(JsonObject):
+ component = StringProperty(default="jre-legacy")
+ majorVersion = IntegerProperty(default=8)
+
+
+class UnknownVersionException(Exception):
+ """Exception raised for unknown Mojang version file format versions.
+
+ Attributes:
+ message -- explanation of the error
+ """
+
+ def __init__(self, message):
+ self.message = message
+
+
+def validateSupportedMojangVersion(version):
+ supportedVersion = 21
+ if version > supportedVersion:
+ raise UnknownVersionException(
+ "Unsupported Mojang format version: %d. Max supported is: %d" % (version, supportedVersion))
+
+
+class MojangVersionFile(JsonObject):
+ arguments = ObjectProperty(MojangArguments, exclude_if_none=True, default=None)
+ assetIndex = ObjectProperty(MojangAssets, exclude_if_none=True, default=None)
+ assets = StringProperty(exclude_if_none=True, default=None)
+ downloads = DictProperty(MojangArtifactBase, exclude_if_none=True, default=None)
+ id = StringProperty(exclude_if_none=True, default=None)
+ libraries = ListProperty(MojangLibrary, exclude_if_none=True, default=None)
+ mainClass = StringProperty(exclude_if_none=True, default=None)
+ processArguments = StringProperty(exclude_if_none=True, default=None)
+ minecraftArguments = StringProperty(exclude_if_none=True, default=None)
+ minimumLauncherVersion = IntegerProperty(exclude_if_none=True, default=None,
+ validators=validateSupportedMojangVersion)
+ releaseTime = ISOTimestampProperty(exclude_if_none=True, default=None)
+ time = ISOTimestampProperty(exclude_if_none=True, default=None)
+ type = StringProperty(exclude_if_none=True, default=None)
+ inheritsFrom = StringProperty(exclude_if_none=True, default=None)
+ logging = DictProperty(MojangLogging, exclude_if_none=True, default=None)
+ complianceLevel = IntegerProperty(exclude_if_none=True, default=None)
+ javaVersion = ObjectProperty(JavaVersion, exclude_if_none=True, default=None)
+
+
+CurrentPolyMCFormatVersion = 1
+
+
+def validateSupportedPolyMCVersion(version):
+ if version > CurrentPolyMCFormatVersion:
+ raise UnknownVersionException(
+ "Unsupported PolyMC format version: %d. Max supported is: %d" % (version, CurrentPolyMCFormatVersion))
+
+
+class PolyMCLibrary(MojangLibrary):
+ url = StringProperty(exclude_if_none=True, default=None)
+ mmcHint = StringProperty(name="MMC-hint", exclude_if_none=True, default=None) # this is supposed to be MMC-hint!
+
+
+class VersionedJsonObject(JsonObject):
+ formatVersion = IntegerProperty(default=CurrentPolyMCFormatVersion, validators=validateSupportedPolyMCVersion)
+
+
+class DependencyEntry(JsonObject):
+ uid = StringProperty(required=True)
+ equals = StringProperty(exclude_if_none=True, default=None)
+ suggests = StringProperty(exclude_if_none=True, default=None)
+
+
+class PolyMCVersionFile(VersionedJsonObject):
+ name = StringProperty(required=True)
+ version = StringProperty(required=True)
+ uid = StringProperty(required=True)
+ requires = ListProperty(DependencyEntry, exclude_if_none=True, default=None)
+ conflicts = ListProperty(DependencyEntry, exclude_if_none=True, default=None)
+ volatile = BooleanProperty(exclude_if_none=True, default=None)
+ assetIndex = ObjectProperty(MojangAssets, exclude_if_none=True, default=None)
+ libraries = ListProperty(PolyMCLibrary, exclude_if_none=True, default=None)
+ mavenFiles = ListProperty(PolyMCLibrary, exclude_if_none=True, default=None)
+ mainJar = ObjectProperty(PolyMCLibrary, exclude_if_none=True, default=None)
+ jarMods = ListProperty(PolyMCLibrary, exclude_if_none=True, default=None)
+ mainClass = StringProperty(exclude_if_none=True, default=None)
+ appletClass = StringProperty(exclude_if_none=True, default=None)
+ minecraftArguments = StringProperty(exclude_if_none=True, default=None)
+ releaseTime = ISOTimestampProperty(exclude_if_none=True, default=None)
+ type = StringProperty(exclude_if_none=True, default=None)
+ compatibleJavaMajors = ListProperty(int, exclude_if_none=True, default=None)
+ addTraits = ListProperty(StringProperty, name="+traits", exclude_if_none=True, default=None)
+ addTweakers = ListProperty(StringProperty, name="+tweakers", exclude_if_none=True, default=None)
+ order = IntegerProperty(exclude_if_none=True, default=None)
+
+
+class UnknownComplianceLevelException(Exception):
+ """Exception raised for unknown Mojang compliance level
+
+ Attributes:
+ message -- explanation of the error
+ """
+
+ def __init__(self, message):
+ self.message = message
+
+
+# Convert Mojang version file object to a PolyMC version file object
+def MojangToPolyMC(file, name, uid, version):
+ pmcFile = PolyMCVersionFile(
+ {
+ "name": name,
+ "uid": uid,
+ "version": version
+ }
+ )
+ pmcFile.assetIndex = file.assetIndex
+ pmcFile.libraries = file.libraries
+ pmcFile.mainClass = file.mainClass
+ if file.id:
+ mainJar = PolyMCLibrary(
+ {
+ "name": "com.mojang:minecraft:%s:client" % file.id,
+ }
+ )
+ cldl = file.downloads['client']
+ mainJar.downloads = MojangLibraryDownloads()
+ mainJar.downloads.artifact = MojangArtifact()
+ mainJar.downloads.artifact.path = None
+ mainJar.downloads.artifact.url = cldl.url
+ mainJar.downloads.artifact.sha1 = cldl.sha1
+ mainJar.downloads.artifact.size = cldl.size
+ pmcFile.mainJar = mainJar
+
+ pmcFile.minecraftArguments = file.minecraftArguments
+ pmcFile.releaseTime = file.releaseTime
+ # time should not be set.
+ pmcFile.type = file.type
+
+ if file.javaVersion is not None: # some versions don't have this. TODO: maybe maintain manual overrides
+ major = file.javaVersion.majorVersion
+ pmcFile.compatibleJavaMajors = [major]
+ if major == 16: # TODO: deal with this somewhere else
+ pmcFile.compatibleJavaMajors.append(17)
+
+ maxSupportedLevel = 1
+ if file.complianceLevel:
+ if file.complianceLevel == 0:
+ pass
+ elif file.complianceLevel == 1:
+ if not pmcFile.addTraits:
+ pmcFile.addTraits = []
+ pmcFile.addTraits.append("XR:Initial")
+ else:
+ raise UnknownComplianceLevelException("Unsupported Mojang compliance level: %d. Max supported is: %d" % (
+ file.complianceLevel, maxSupportedLevel))
+ return pmcFile
+
+
+class PolyMCSharedPackageData(VersionedJsonObject):
+ name = StringProperty(required=True)
+ uid = StringProperty(required=True)
+ recommended = ListProperty(StringProperty, exclude_if_none=True, default=None)
+ authors = ListProperty(StringProperty, exclude_if_none=True, default=None)
+ description = StringProperty(exclude_if_none=True, default=None)
+ projectUrl = StringProperty(exclude_if_none=True, default=None)
+
+ def write(self):
+ try:
+ with open(PMC_DIR + "/%s/package.json" % self.uid, 'w') as file:
+ json.dump(self.to_json(), file, sort_keys=True, indent=4)
+ except EnvironmentError as e:
+ print("Error while trying to save shared packaged data for %s:" % self.uid, e)
+
+
+def writeSharedPackageData(uid, name):
+ desc = PolyMCSharedPackageData({
+ 'name': name,
+ 'uid': uid
+ })
+ with open(PMC_DIR + "/%s/package.json" % uid, 'w') as file:
+ json.dump(desc.to_json(), file, sort_keys=True, indent=4)
+
+
+def readSharedPackageData(uid):
+ with open(PMC_DIR + "/%s/package.json" % uid, 'r') as file:
+ return PolyMCSharedPackageData(json.load(file))
+
+
+class PolyMCVersionIndexEntry(JsonObject):
+ version = StringProperty()
+ type = StringProperty(exclude_if_none=True, default=None)
+ releaseTime = ISOTimestampProperty()
+ requires = ListProperty(DependencyEntry, exclude_if_none=True, default=None)
+ conflicts = ListProperty(DependencyEntry, exclude_if_none=True, default=None)
+ recommended = BooleanProperty(exclude_if_none=True, default=None)
+ volatile = BooleanProperty(exclude_if_none=True, default=None)
+ sha256 = StringProperty()
+
+
+class PolyMCVersionIndex(VersionedJsonObject):
+ name = StringProperty()
+ uid = StringProperty()
+ versions = ListProperty(PolyMCVersionIndexEntry)
+
+
+class PolyMCPackageIndexEntry(JsonObject):
+ name = StringProperty()
+ uid = StringProperty()
+ sha256 = StringProperty()
+
+
+class PolyMCPackageIndex(VersionedJsonObject):
+ packages = ListProperty(PolyMCPackageIndexEntry)
+
+
+'''
+The PolyMC static override file for legacy looks like this:
+{
+ "versions": [
+ ...
+ {
+ "id": "c0.0.13a",
+ "checksum": "3617fbf5fbfd2b837ebf5ceb63584908",
+ "releaseTime": "2009-05-31T00:00:00+02:00",
+ "type": "old_alpha",
+ "mainClass": "com.mojang.minecraft.Minecraft",
+ "appletClass": "com.mojang.minecraft.MinecraftApplet",
+ "+traits": ["legacyLaunch", "no-texturepacks"]
+ },
+ ...
+ ]
+}
+'''
+
+
+class LegacyOverrideEntry(JsonObject):
+ releaseTime = ISOTimestampProperty(exclude_if_none=True, default=None)
+ mainClass = StringProperty(exclude_if_none=True, default=None)
+ appletClass = StringProperty(exclude_if_none=True, default=None)
+ addTraits = ListProperty(StringProperty, name="+traits", exclude_if_none=True, default=None)
+
+
+class LegacyOverrideIndex(JsonObject):
+ versions = DictProperty(LegacyOverrideEntry)
+
+
+def ApplyLegacyOverride(pmcFile, legacyOverride):
+ # simply hard override classes
+ pmcFile.mainClass = legacyOverride.mainClass
+ pmcFile.appletClass = legacyOverride.appletClass
+ # if we have an updated release time (more correct than Mojang), use it
+ if legacyOverride.releaseTime != None:
+ pmcFile.releaseTime = legacyOverride.releaseTime
+ # add traits, if any
+ if legacyOverride.addTraits:
+ if not pmcFile.addTraits:
+ pmcFile.addTraits = []
+ pmcFile.addTraits = pmcFile.addTraits + legacyOverride.addTraits
+ # remove all libraries - they are not needed for legacy
+ pmcFile.libraries = None
+ # remove minecraft arguments - we use our own hardcoded ones
+ pmcFile.minecraftArguments = None