odse 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ods_e/__init__.py +13 -0
- ods_e/transformer.py +139 -0
- ods_e/validator.py +185 -0
- odse-0.1.0.dist-info/METADATA +118 -0
- odse-0.1.0.dist-info/RECORD +8 -0
- odse-0.1.0.dist-info/WHEEL +5 -0
- odse-0.1.0.dist-info/entry_points.txt +2 -0
- odse-0.1.0.dist-info/top_level.txt +1 -0
ods_e/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ODS-E: Open Data Schema for Energy
|
|
3
|
+
|
|
4
|
+
A Python library for validating and transforming energy asset data
|
|
5
|
+
using the ODS-E specification.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
from .validator import validate, validate_file
|
|
11
|
+
from .transformer import transform, transform_stream
|
|
12
|
+
|
|
13
|
+
__all__ = ["validate", "validate_file", "transform", "transform_stream"]
|
ods_e/transformer.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ODS-E Transformer
|
|
3
|
+
|
|
4
|
+
Transforms OEM-specific data formats to ODS-E schema.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def transform(
|
|
12
|
+
data: Union[str, Path],
|
|
13
|
+
source: str,
|
|
14
|
+
asset_id: Optional[str] = None,
|
|
15
|
+
timezone: Optional[str] = None,
|
|
16
|
+
) -> List[Dict[str, Any]]:
|
|
17
|
+
"""
|
|
18
|
+
Transform OEM data to ODS-E format.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data: Path to data file or data string
|
|
22
|
+
source: OEM source identifier (e.g., "huawei", "enphase", "solarman")
|
|
23
|
+
asset_id: Optional asset identifier to include in output
|
|
24
|
+
timezone: Optional timezone for timestamp conversion
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of ODS-E formatted records
|
|
28
|
+
"""
|
|
29
|
+
transformer = _get_transformer(source)
|
|
30
|
+
return transformer.transform(data, asset_id=asset_id, timezone=timezone)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def transform_stream(
|
|
34
|
+
data: Union[str, Path],
|
|
35
|
+
source: str,
|
|
36
|
+
**kwargs,
|
|
37
|
+
) -> Iterator[Dict[str, Any]]:
|
|
38
|
+
"""
|
|
39
|
+
Stream transform OEM data to ODS-E format.
|
|
40
|
+
|
|
41
|
+
Useful for large files where loading all records into memory
|
|
42
|
+
is not practical.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
data: Path to data file
|
|
46
|
+
source: OEM source identifier
|
|
47
|
+
**kwargs: Additional arguments passed to transformer
|
|
48
|
+
|
|
49
|
+
Yields:
|
|
50
|
+
ODS-E formatted records one at a time
|
|
51
|
+
"""
|
|
52
|
+
transformer = _get_transformer(source)
|
|
53
|
+
yield from transformer.transform_stream(data, **kwargs)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_transformer(source: str):
|
|
57
|
+
"""Get the appropriate transformer for the source."""
|
|
58
|
+
transformers = {
|
|
59
|
+
"huawei": HuaweiTransformer(),
|
|
60
|
+
"enphase": EnphaseTransformer(),
|
|
61
|
+
"solarman": SolarmanTransformer(),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
source_lower = source.lower()
|
|
65
|
+
if source_lower not in transformers:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Unknown source '{source}'. "
|
|
68
|
+
f"Supported sources: {list(transformers.keys())}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return transformers[source_lower]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BaseTransformer:
|
|
75
|
+
"""Base class for OEM transformers."""
|
|
76
|
+
|
|
77
|
+
def transform(
|
|
78
|
+
self,
|
|
79
|
+
data: Union[str, Path],
|
|
80
|
+
**kwargs,
|
|
81
|
+
) -> List[Dict[str, Any]]:
|
|
82
|
+
"""Transform data to ODS-E format."""
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
def transform_stream(
|
|
86
|
+
self,
|
|
87
|
+
data: Union[str, Path],
|
|
88
|
+
**kwargs,
|
|
89
|
+
) -> Iterator[Dict[str, Any]]:
|
|
90
|
+
"""Stream transform data to ODS-E format."""
|
|
91
|
+
# Default implementation: yield from transform()
|
|
92
|
+
yield from self.transform(data, **kwargs)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class HuaweiTransformer(BaseTransformer):
|
|
96
|
+
"""Transform Huawei FusionSolar data to ODS-E."""
|
|
97
|
+
|
|
98
|
+
ERROR_CODES = {
|
|
99
|
+
"normal": [0, 1, 2, 3, 256, 512, 1025, 1026, 1280, 1281, 1536, 1792, 2048, 2304, 40960, 49152],
|
|
100
|
+
"warning": [513, 514, 772, 773, 774],
|
|
101
|
+
"critical": [768, 770, 771, 45056],
|
|
102
|
+
"fault": [769, 1024],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def transform(self, data: Union[str, Path], **kwargs) -> List[Dict[str, Any]]:
|
|
106
|
+
# Placeholder implementation
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
def _map_error_code(self, code: int) -> str:
|
|
110
|
+
for error_type, codes in self.ERROR_CODES.items():
|
|
111
|
+
if code in codes:
|
|
112
|
+
return error_type
|
|
113
|
+
return "unknown"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class EnphaseTransformer(BaseTransformer):
|
|
117
|
+
"""Transform Enphase Envoy data to ODS-E."""
|
|
118
|
+
|
|
119
|
+
def transform(self, data: Union[str, Path], **kwargs) -> List[Dict[str, Any]]:
|
|
120
|
+
# Placeholder implementation
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SolarmanTransformer(BaseTransformer):
|
|
125
|
+
"""Transform Solarman Logger data to ODS-E."""
|
|
126
|
+
|
|
127
|
+
STATE_MAPPING = {
|
|
128
|
+
"Normal": "normal",
|
|
129
|
+
"Operating": "normal",
|
|
130
|
+
"Warning": "warning",
|
|
131
|
+
"Fault": "fault",
|
|
132
|
+
"Error": "fault",
|
|
133
|
+
"Offline": "offline",
|
|
134
|
+
"Standby": "standby",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def transform(self, data: Union[str, Path], **kwargs) -> List[Dict[str, Any]]:
|
|
138
|
+
# Placeholder implementation
|
|
139
|
+
return []
|
ods_e/validator.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ODS-E Validator
|
|
3
|
+
|
|
4
|
+
Validates energy data against ODS-E JSON schemas.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional, Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ValidationError:
|
|
15
|
+
"""Represents a validation error."""
|
|
16
|
+
path: str
|
|
17
|
+
message: str
|
|
18
|
+
code: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ValidationResult:
|
|
23
|
+
"""Result of a validation operation."""
|
|
24
|
+
is_valid: bool
|
|
25
|
+
errors: List[ValidationError]
|
|
26
|
+
warnings: List[ValidationError]
|
|
27
|
+
level: str = "schema"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate(
|
|
31
|
+
data: Union[dict, str, Path],
|
|
32
|
+
level: str = "schema",
|
|
33
|
+
capacity_kw: Optional[float] = None,
|
|
34
|
+
latitude: Optional[float] = None,
|
|
35
|
+
longitude: Optional[float] = None,
|
|
36
|
+
) -> ValidationResult:
|
|
37
|
+
"""
|
|
38
|
+
Validate data against ODS-E production-timeseries schema.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
data: Dictionary, JSON string, or path to JSON file
|
|
42
|
+
level: Validation level - "schema" or "semantic"
|
|
43
|
+
capacity_kw: Asset capacity in kW (required for semantic validation)
|
|
44
|
+
latitude: Asset latitude (optional, for nighttime checks)
|
|
45
|
+
longitude: Asset longitude (optional, for nighttime checks)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
ValidationResult with is_valid status and any errors/warnings
|
|
49
|
+
"""
|
|
50
|
+
# Parse input
|
|
51
|
+
if isinstance(data, (str, Path)):
|
|
52
|
+
if Path(data).exists():
|
|
53
|
+
with open(data) as f:
|
|
54
|
+
data = json.load(f)
|
|
55
|
+
else:
|
|
56
|
+
data = json.loads(data)
|
|
57
|
+
|
|
58
|
+
errors = []
|
|
59
|
+
warnings = []
|
|
60
|
+
|
|
61
|
+
# Schema validation
|
|
62
|
+
errors.extend(_validate_schema(data))
|
|
63
|
+
|
|
64
|
+
# Semantic validation
|
|
65
|
+
if level == "semantic" and not errors:
|
|
66
|
+
sem_errors, sem_warnings = _validate_semantic(
|
|
67
|
+
data, capacity_kw, latitude, longitude
|
|
68
|
+
)
|
|
69
|
+
errors.extend(sem_errors)
|
|
70
|
+
warnings.extend(sem_warnings)
|
|
71
|
+
|
|
72
|
+
return ValidationResult(
|
|
73
|
+
is_valid=len(errors) == 0,
|
|
74
|
+
errors=errors,
|
|
75
|
+
warnings=warnings,
|
|
76
|
+
level=level,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def validate_file(
|
|
81
|
+
file_path: Union[str, Path],
|
|
82
|
+
level: str = "schema",
|
|
83
|
+
**kwargs,
|
|
84
|
+
) -> ValidationResult:
|
|
85
|
+
"""
|
|
86
|
+
Validate a JSON file containing ODS-E data.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
file_path: Path to JSON file
|
|
90
|
+
level: Validation level
|
|
91
|
+
**kwargs: Additional arguments passed to validate()
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
ValidationResult
|
|
95
|
+
"""
|
|
96
|
+
return validate(Path(file_path), level=level, **kwargs)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _validate_schema(data: dict) -> List[ValidationError]:
|
|
100
|
+
"""Validate against JSON schema."""
|
|
101
|
+
errors = []
|
|
102
|
+
|
|
103
|
+
# Required fields
|
|
104
|
+
required = ["timestamp", "kWh", "error_type"]
|
|
105
|
+
for field in required:
|
|
106
|
+
if field not in data:
|
|
107
|
+
errors.append(ValidationError(
|
|
108
|
+
path=f"$.{field}",
|
|
109
|
+
message=f"Required field '{field}' is missing",
|
|
110
|
+
code="REQUIRED_FIELD_MISSING",
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
if errors:
|
|
114
|
+
return errors
|
|
115
|
+
|
|
116
|
+
# Type validation
|
|
117
|
+
if not isinstance(data.get("kWh"), (int, float)):
|
|
118
|
+
errors.append(ValidationError(
|
|
119
|
+
path="$.kWh",
|
|
120
|
+
message=f"Expected number, got {type(data.get('kWh')).__name__}",
|
|
121
|
+
code="TYPE_MISMATCH",
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
# Enum validation
|
|
125
|
+
valid_error_types = [
|
|
126
|
+
"normal", "warning", "critical", "fault",
|
|
127
|
+
"offline", "standby", "unknown"
|
|
128
|
+
]
|
|
129
|
+
if data.get("error_type") not in valid_error_types:
|
|
130
|
+
errors.append(ValidationError(
|
|
131
|
+
path="$.error_type",
|
|
132
|
+
message=f"Value '{data.get('error_type')}' not in enum {valid_error_types}",
|
|
133
|
+
code="ENUM_MISMATCH",
|
|
134
|
+
))
|
|
135
|
+
|
|
136
|
+
# Bounds validation
|
|
137
|
+
if isinstance(data.get("kWh"), (int, float)) and data["kWh"] < 0:
|
|
138
|
+
errors.append(ValidationError(
|
|
139
|
+
path="$.kWh",
|
|
140
|
+
message="kWh must be >= 0",
|
|
141
|
+
code="OUT_OF_BOUNDS",
|
|
142
|
+
))
|
|
143
|
+
|
|
144
|
+
if "PF" in data:
|
|
145
|
+
pf = data["PF"]
|
|
146
|
+
if isinstance(pf, (int, float)) and (pf < 0 or pf > 1):
|
|
147
|
+
errors.append(ValidationError(
|
|
148
|
+
path="$.PF",
|
|
149
|
+
message="Power factor must be between 0 and 1",
|
|
150
|
+
code="OUT_OF_BOUNDS",
|
|
151
|
+
))
|
|
152
|
+
|
|
153
|
+
return errors
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _validate_semantic(
|
|
157
|
+
data: dict,
|
|
158
|
+
capacity_kw: Optional[float],
|
|
159
|
+
latitude: Optional[float],
|
|
160
|
+
longitude: Optional[float],
|
|
161
|
+
) -> tuple:
|
|
162
|
+
"""Validate semantic constraints."""
|
|
163
|
+
errors = []
|
|
164
|
+
warnings = []
|
|
165
|
+
|
|
166
|
+
# Physical bounds check
|
|
167
|
+
if capacity_kw and isinstance(data.get("kWh"), (int, float)):
|
|
168
|
+
# Assume 1-hour interval for simplicity
|
|
169
|
+
max_kwh = capacity_kw * 1.1
|
|
170
|
+
if data["kWh"] > max_kwh:
|
|
171
|
+
warnings.append(ValidationError(
|
|
172
|
+
path="$.kWh",
|
|
173
|
+
message=f"kWh ({data['kWh']}) exceeds maximum possible ({max_kwh}) for {capacity_kw}kW capacity",
|
|
174
|
+
code="EXCEEDS_PHYSICAL_MAXIMUM",
|
|
175
|
+
))
|
|
176
|
+
|
|
177
|
+
# State/production consistency
|
|
178
|
+
if data.get("error_type") == "offline" and data.get("kWh", 0) > 10:
|
|
179
|
+
warnings.append(ValidationError(
|
|
180
|
+
path="$",
|
|
181
|
+
message=f"Significant production ({data['kWh']} kWh) reported with error_type 'offline'",
|
|
182
|
+
code="STATE_PRODUCTION_MISMATCH",
|
|
183
|
+
))
|
|
184
|
+
|
|
185
|
+
return errors, warnings
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open Data Schema for Energy - validation and transformation library
|
|
5
|
+
Author-email: Asoba Corporation <support@asoba.co>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/AsobaCloud/odse
|
|
8
|
+
Project-URL: Documentation, https://docs.asoba.co/ona-protocol/overview
|
|
9
|
+
Project-URL: Repository, https://github.com/AsobaCloud/odse
|
|
10
|
+
Keywords: energy,solar,iot,data,schema,validation
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
30
|
+
|
|
31
|
+
# ODS-E: Open Data Schema for Energy
|
|
32
|
+
|
|
33
|
+
[](https://creativecommons.org/licenses/by-sa/4.0/)
|
|
34
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
35
|
+
|
|
36
|
+
ODS-E is an open specification for standardizing energy asset data from IoT devices, enabling interoperability across the renewable energy ecosystem.
|
|
37
|
+
|
|
38
|
+
## Why ODS-E?
|
|
39
|
+
|
|
40
|
+
- **No Vendor Lock-in**: Your data works with any ODS-E compatible system
|
|
41
|
+
- **Faster Integrations**: Pre-built transforms for common OEMs (Huawei, Enphase, Solarman)
|
|
42
|
+
- **Analytics-Ready**: Standardized error taxonomy and semantic validation
|
|
43
|
+
- **Future-Proof**: CC-BY-SA licensed specification ensures extensions stay open
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install odse
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from ods_e import validate, transform
|
|
53
|
+
|
|
54
|
+
# Validate ODS-E data
|
|
55
|
+
result = validate("production_data.json")
|
|
56
|
+
|
|
57
|
+
# Transform from OEM format
|
|
58
|
+
ods_data = transform("huawei_export.csv", source="huawei")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Repository Structure
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
ona-protocol/
|
|
65
|
+
├── LICENSE-SPEC.md # CC-BY-SA 4.0 (specification, schemas, transforms)
|
|
66
|
+
├── LICENSE-CODE.md # Apache 2.0 (reference implementation, tools)
|
|
67
|
+
├── spec/ # Specification documents
|
|
68
|
+
├── schemas/ # JSON Schema definitions
|
|
69
|
+
├── transforms/ # OEM transform specifications
|
|
70
|
+
├── src/ # Reference implementation
|
|
71
|
+
└── tools/ # CLI tools
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Core Schema
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"timestamp": "2026-02-05T14:00:00Z",
|
|
79
|
+
"kWh": 847.5,
|
|
80
|
+
"error_type": "normal",
|
|
81
|
+
"PF": 0.98
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Required fields:**
|
|
86
|
+
- `timestamp` - ISO 8601 with timezone
|
|
87
|
+
- `kWh` - Active energy (≥ 0)
|
|
88
|
+
- `error_type` - One of: `normal`, `warning`, `critical`, `fault`, `offline`, `standby`, `unknown`
|
|
89
|
+
|
|
90
|
+
## Supported OEMs
|
|
91
|
+
|
|
92
|
+
| OEM | Format | Status |
|
|
93
|
+
|-----|--------|--------|
|
|
94
|
+
| Huawei FusionSolar | CSV, API | ✅ Included |
|
|
95
|
+
| Enphase Envoy | JSON, API | ✅ Included |
|
|
96
|
+
| Solarman Logger | CSV | ✅ Included |
|
|
97
|
+
| SolarEdge | JSON API | ✅ Included |
|
|
98
|
+
| Fronius | JSON API | ✅ Included |
|
|
99
|
+
| Switch Energy | CSV | ✅ Included |
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
- **Specification, Schemas, Transforms**: [CC-BY-SA 4.0](LICENSE-SPEC.md)
|
|
104
|
+
- **Reference Implementation, Tools**: [Apache 2.0](LICENSE-CODE.md)
|
|
105
|
+
|
|
106
|
+
## Documentation
|
|
107
|
+
|
|
108
|
+
- [Full Documentation](https://docs.asoba.co/ona-protocol)
|
|
109
|
+
- [Schema Reference](https://docs.asoba.co/ona-protocol/schemas)
|
|
110
|
+
- [Transform Guide](https://docs.asoba.co/ona-protocol/transforms)
|
|
111
|
+
|
|
112
|
+
## Contributing
|
|
113
|
+
|
|
114
|
+
Contributions are welcome. Schema and transform contributions must be licensed under CC-BY-SA 4.0.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
Maintained by [Asoba Corporation](https://asoba.co)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ods_e/__init__.py,sha256=1cgVXnHduy-sFJL6IeexDekVE9J1MZccMkUen0FoaNQ,340
|
|
2
|
+
ods_e/transformer.py,sha256=H1PLEFKORTq99HGpDnzKInoqYb5u276iELq6L1bRUC0,3826
|
|
3
|
+
ods_e/validator.py,sha256=oXLCH1Jqpa9HPwjz9DqW0Gzp4Z-qh-D-wp6ehG0m1VQ,5131
|
|
4
|
+
odse-0.1.0.dist-info/METADATA,sha256=yna_LYJo5krC2uNKaxQyOxx-guDif-EfLArtSpuxwcA,3989
|
|
5
|
+
odse-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
odse-0.1.0.dist-info/entry_points.txt,sha256=Q57qBjBH3FOsovuCw3cXGcxveUPKsp2CTLnm0CEwEpk,40
|
|
7
|
+
odse-0.1.0.dist-info/top_level.txt,sha256=HrO3os1RN2ICfcAKoeXAxwvQ0C9nz8q7_kF8aYGCTbM,6
|
|
8
|
+
odse-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ods_e
|