Skip to content

Commit

Permalink
Fixes issue #171 (#172)
Browse files Browse the repository at this point in the history
Fixed issue #173
  • Loading branch information
joelebwf authored Jun 4, 2019
1 parent 676e877 commit b4c6019
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 19 deletions.
13 changes: 10 additions & 3 deletions oblib/data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ def __init__(self, ob_instance, table_name):
# self.contexts stores a list of contexts that have been populated within
# this table instance.
self.contexts = []
self.ts = ob_instance.ts

relationships = ob_instance.relations
# Use the relationships to find the names of my axes:
Expand Down Expand Up @@ -271,10 +272,12 @@ def _is_valid_context(self, context):
raise OBContextError("{} is not a valid Context instance".format(context))

for axis_name in self._axes:
if not axis_name in context.axes:
if self.ts.get_concept_details(axis_name).typed_domain_ref and not axis_name in context.axes:
raise OBContextError(
"Missing required {} axis for table {}".format(
axis_name, self._table_name))
elif not self.ts.get_concept_details(axis_name).typed_domain_ref and not axis_name in context.axes:
continue

# Check that the value is not outside the domain, for domain-based axes:
axis = self._axes[axis_name]
Expand Down Expand Up @@ -1180,12 +1183,16 @@ def _is_valid_unit(self, concept_name, unit_id):
Raises:
OBUnitError explaining why the unit is not valid.
"""
# TODO Refactor to move this logic into the Concept class?
# TODO Refactor to move this logic into the Concept class or place in Parser?
# TODO Examine full definition of valid units and update logic to be completely equitable

unitlessTypes = ["xbrli:integerItemType", "xbrli:stringItemType",
"xbrli:decimalItemType", "xbrli:booleanItemType",
"xbrli:dateItemType", "num:percentItemType",
"xbrli:anyURIItemType"]
"xbrli:anyURIItemType", "dei:legalEntityIdentifierItemType"]
# NOTE: As a quick fix dei:legalEntityIdentifier Type has been added so that the sample programs
# works but this is a case of using hardcoding as opposed to correct logic.

# There is type-checking we can do for these unitless types but we'll handle
# it elsewhere

Expand Down
2 changes: 2 additions & 0 deletions oblib/taxonomy.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def __init__(self):
self.period_independent = None
self.substitution_group = None
self.type_name = None
self.typed_domain_ref = None
self.period_type = None

def __repr__(self):
Expand All @@ -119,6 +120,7 @@ def __repr__(self):
"," + str(self.period_independent) + \
"," + str(self.substitution_group) + \
"," + str(self.type_name) + \
"," + str(self.typed_domain_ref) + \
"," + str(self.period_type) + \
"}"

Expand Down
5 changes: 2 additions & 3 deletions oblib/taxonomy_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,10 @@ def startElement(self, name, attrs):
element.substitution_group = taxonomy.SubstitutionGroup(item[1])
elif item[0] == "type":
element.type_name = item[1]
elif item[0] == "xbrli:periodType":
# element.period_type = item[1]
element.period_type = taxonomy.PeriodType(item[1])
elif item[0] == "xbrldt:typedDomainRef":
element.typed_domain_ref = item[1]
elif item[0] == "xbrli:periodType":
element.period_type = taxonomy.PeriodType(item[1])
self._elements[element.id] = element

def elements(self):
Expand Down
51 changes: 47 additions & 4 deletions oblib/tests/test_data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,12 @@ def test_is_valid_context_axes(self):
# The context must also provide all of the axes needed to place the
# fact within the right table.

# DeviceCost is on the CutSheetDetailsTable so it needs a value
# for ProductIdentifierAxis and TestConditionAxis.
# Context is required
with self.assertRaises(ob.OBContextError):
doc._is_valid_context("solar:DeviceCost", {})

# DeviceCost is on the CutSheetDetailsTable so it needs a value
# for the required ProductIdentifierAxis but not for the optional TestConditionAxis.
context = data_model.Context(instant = datetime.now(),
ProductIdentifierAxis = "placeholder",
TestConditionAxis = "solar:StandardTestConditionMember")
Expand All @@ -161,8 +162,7 @@ def test_is_valid_context_axes(self):

badContext = data_model.Context(instant = datetime.now(),
ProductIdentifierAxis = "placeholder")
with self.assertRaises(ob.OBContextError):
doc._is_valid_context("solar:DeviceCost", badContext)
doc._is_valid_context("solar:DeviceCost", badContext)

# How do we know what are valid values for ProductIdentifierAxis and
# TestConditionAxis? (I think they are meant to be UUIDs.)
Expand Down Expand Up @@ -991,3 +991,46 @@ def test_ct_issue(self):
'us-gaap:SaleLeasebackTransactionDescriptionAxis': 'us-gaap:SaleLeasebackTransactionNameDomain',
'solar:ProjectIdentifierAxis': '1'}
doc.set('us-gaap:SaleLeasebackTransactionDescription', 'Sample String', **kwargs)

def test_optional_required_axis(self):
# Tests that an optional axis can be either present or not in input data.

# Required With Axis
doc = data_model.OBInstance("System", self.taxonomy)
kwargs = {'duration': 'forever', 'entity': 'PLUTO',
'solar:TestConditionAxis': 'solar:StandardTestConditionMember',
'solar:ProductIdentifierAxis': '1',
'solar:PVSystemIdentifierAxis': '1'}
doc.set('solar:ProductName', 'Sample Product', **kwargs)

# Required Without Axis
kwargs = {'duration': 'forever', 'entity': 'PLUTO',
'solar:TestConditionAxis': 'solar:StandardTestConditionMember',
'solar:ProductIdentifierAxis': '1'}
with self.assertRaises(ob.OBContextError):
doc.set('solar:ProductName', 'Sample Product', **kwargs)

# Optional With Axis
doc = data_model.OBInstance("IECRECertificate", self.taxonomy)
kwargs = {'duration': 'forever', 'entity': 'PLUTO',
'solar:PowerPurchaseAgreementContractAxis': '1',
'solar:EnergyContractYearlyRateAxis': '1',
'solar:MonthlyPeriodAxis': '1',
'unit_name': 'USD'}
doc.set('solar:EnergyCharge', '10500.26', **kwargs)

# Optional Without Axis
kwargs = {'duration': 'forever', 'entity': 'PLUTO',
'solar:PowerPurchaseAgreementContractAxis': '1',
'solar:EnergyContractYearlyRateAxis': '1',
'unit_name': 'USD'}
doc.set('solar:EnergyCharge', '10500.26', **kwargs)

def test_set_LEI(self):
# Tests that setting a LEI does not require a unit (a bug fix)

doc = data_model.OBInstance("Utility", self.taxonomy)
kwargs = {'duration': 'forever', 'entity': 'PLUTO',
'solar:UtilityIdentifierAxis': '1'}
# doc.set('solar:UtilityIdentifier', '1234567890ABCDEFGHIJ', **kwargs)
doc.set('solar:UtilityIdentifier', '12345678901234567890', **kwargs)
11 changes: 10 additions & 1 deletion oblib/tests/test_taxonomy.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ def test_concept_details(self):
self.assertIsInstance(ci.type_name, string_types)
self.assertIsInstance(ci.period_type, taxonomy.PeriodType)

ci = tax.semantic.get_concept_details("solar:MonthlyPeriodAxis")
self.assertIsNone(ci.typed_domain_ref)

ci = tax.semantic.get_concept_details("solar:PVSystemIdentifierAxis")
self.assertIsInstance(ci.typed_domain_ref, string_types)

# Values checks
ci = tax.semantic.get_concept_details("solar:ACDisconnectSwitchMember")
self.assertIsNotNone(ci)
self.assertTrue(ci.abstract)
Expand All @@ -230,7 +237,6 @@ def test_concept_details(self):
self.assertEqual(ci.type_name, "nonnum:domainItemType")
self.assertEqual(ci.period_type, taxonomy.PeriodType.duration)

# Values checks
ci = tax.semantic.get_concept_details("solar:AdvisorInvoicesCounterparties")
self.assertIsNotNone(ci)
self.assertFalse(ci.abstract)
Expand All @@ -253,6 +259,9 @@ def test_concept_details(self):
self.assertEqual(ci.type_name, "dei:legalEntityIdentifierItemType")
self.assertEqual(ci.period_type, taxonomy.PeriodType.duration)

ci = tax.semantic.get_concept_details("solar:PVSystemIdentifierAxis")
self.assertEqual(ci.typed_domain_ref, "#solar_PVSystemIdentifierDomain")

with self.assertRaises(KeyError):
_ = tax.semantic.get_concept_details("solar:iamnotaconcept")

Expand Down
4 changes: 2 additions & 2 deletions oblib/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ def validate_concept_value(self, concept_details, value):
.format(concept_details.type_name, method_name))

# Check identifiers. This is based upon the name of the field containing
# the word Identifier in it.
if concept_details.id.find("Identifier") != -1:
# the word Identifier in it. Avoid UtilityIdentifier which is a LEI.
if concept_details.id != "solar:UtilityIdentifier" and concept_details.id.find("Identifier") != -1:
if not identifier.validate(value):
errors += ["'{}' is not valid identifier.".format(concept_details.id)]

Expand Down
13 changes: 7 additions & 6 deletions scripts/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def list_concept_details(args):
print("Period Independent:", c.period_independent)
print("Substitution Group:", c.substitution_group.value)
print("Type: ", c.type_name)
print("Typed Domain Ref: ", c.typed_domain_ref)
print("Period Type: ", c.period_type.value)
else:
print("Not found")
Expand Down Expand Up @@ -144,22 +145,22 @@ def list_entrypoint_concepts_details(args):
"Substitution Group, Type, Period Type")
for c in concepts_details:
d = concepts_details[c]
print('%s, %s, %s, %s, %s, %s, %s, %s' %
print('%s, %s, %s, %s, %s, %s, %s, %s, %s' %
(d.id, d.name, d.abstract, d.nillable, d.period_independent,
d.substitution_group.value, d.type_name, d.period_type.value))
d.substitution_group.value, d.type_name, d.typed_domain_ref, d.period_type.value))
else:
_, concepts_details = taxonomy.semantic.get_entrypoint_concepts(args.entrypoint,
details=True)
print('%85s %80s %8s %8s %10s %20s %28s %8s' %
("Id", "Name", "Abstract", "Nillable", "Period Ind",
"Substitution Group", "Type", "Period Type"))
print('%0.85s %0.80s %0.8s %0.8s %0.10s %0.20s %0.28s %0.8s' %
(DASHES, DASHES, DASHES, DASHES, DASHES, DASHES, DASHES, DASHES))
print('%0.85s %0.80s %0.8s %0.8s %0.10s %0.20s %0.28s %0.40s %0.8s' %
(DASHES, DASHES, DASHES, DASHES, DASHES, DASHES, DASHES, DASHES, DASHES))
for c in concepts_details:
d = concepts_details[c]
print('%85s %80s %8s %8s %10s %20s %28s %8s' %
print('%85s %80s %8s %8s %10s %20s %28s %40s %8s' %
(d.id, d.name, d.abstract, d.nillable, d.period_independent,
d.substitution_group.value, d.type_name, d.period_type.value))
d.substitution_group.value, d.type_name, d.typed_domain_ref, d.period_type.value))


def list_concepts(args):
Expand Down

0 comments on commit b4c6019

Please sign in to comment.