Merge branch 'parse-cyber-observables' into cyber-observables
commit
ae5fb51564
|
@ -0,0 +1,7 @@
|
||||||
|
[settings]
|
||||||
|
check=1
|
||||||
|
diff=1
|
||||||
|
known_third_party=dateutil,pytest,pytz,six
|
||||||
|
known_first_party=stix2
|
||||||
|
not_skip=__init__.py
|
||||||
|
force_sort_within_sections=1
|
|
@ -179,7 +179,7 @@ class _STIXBase(collections.Mapping):
|
||||||
return self.new_version(revoked=True)
|
return self.new_version(revoked=True)
|
||||||
|
|
||||||
|
|
||||||
class Observable(_STIXBase):
|
class _Observable(_STIXBase):
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
# the constructor might be called independently of an observed data object
|
# the constructor might be called independently of an observed data object
|
||||||
|
@ -187,10 +187,10 @@ class Observable(_STIXBase):
|
||||||
self._STIXBase__valid_refs = kwargs.pop('_valid_refs')
|
self._STIXBase__valid_refs = kwargs.pop('_valid_refs')
|
||||||
else:
|
else:
|
||||||
self._STIXBase__valid_refs = []
|
self._STIXBase__valid_refs = []
|
||||||
super(Observable, self).__init__(**kwargs)
|
super(_Observable, self).__init__(**kwargs)
|
||||||
|
|
||||||
def _check_property(self, prop_name, prop, kwargs):
|
def _check_property(self, prop_name, prop, kwargs):
|
||||||
super(Observable, self)._check_property(prop_name, prop, kwargs)
|
super(_Observable, self)._check_property(prop_name, prop, kwargs)
|
||||||
if prop_name.endswith('_ref') and prop_name in kwargs:
|
if prop_name.endswith('_ref') and prop_name in kwargs:
|
||||||
ref = kwargs[prop_name]
|
ref = kwargs[prop_name]
|
||||||
if ref not in self._STIXBase__valid_refs:
|
if ref not in self._STIXBase__valid_refs:
|
||||||
|
|
|
@ -5,15 +5,15 @@ embedded in Email Message objects, inherit from _STIXBase instead of Observable
|
||||||
and do not have a '_type' attribute.
|
and do not have a '_type' attribute.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .base import Observable, _STIXBase
|
from .base import _Observable, _STIXBase
|
||||||
from .properties import (BinaryProperty, BooleanProperty, DictionaryProperty,
|
from .properties import (BinaryProperty, BooleanProperty, DictionaryProperty,
|
||||||
EmbeddedObjectProperty, HashesProperty, HexProperty,
|
EmbeddedObjectProperty, EnumProperty, HashesProperty,
|
||||||
IntegerProperty, ListProperty,
|
HexProperty, IntegerProperty, ListProperty,
|
||||||
ObjectReferenceProperty, Property, StringProperty,
|
ObjectReferenceProperty, Property, StringProperty,
|
||||||
TimestampProperty, TypeProperty)
|
TimestampProperty, TypeProperty)
|
||||||
|
|
||||||
|
|
||||||
class Artifact(Observable):
|
class Artifact(_Observable):
|
||||||
_type = 'artifact'
|
_type = 'artifact'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -29,7 +29,7 @@ class Artifact(Observable):
|
||||||
self._check_properties_dependency(["hashes"], ["url"])
|
self._check_properties_dependency(["hashes"], ["url"])
|
||||||
|
|
||||||
|
|
||||||
class AutonomousSystem(Observable):
|
class AutonomousSystem(_Observable):
|
||||||
_type = 'autonomous-system'
|
_type = 'autonomous-system'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -39,7 +39,7 @@ class AutonomousSystem(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Directory(Observable):
|
class Directory(_Observable):
|
||||||
_type = 'directory'
|
_type = 'directory'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -53,7 +53,7 @@ class Directory(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DomainName(Observable):
|
class DomainName(_Observable):
|
||||||
_type = 'domain-name'
|
_type = 'domain-name'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -62,7 +62,7 @@ class DomainName(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EmailAddress(Observable):
|
class EmailAddress(_Observable):
|
||||||
_type = 'email-address'
|
_type = 'email-address'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -85,7 +85,7 @@ class EmailMIMEComponent(_STIXBase):
|
||||||
self._check_at_least_one_property(["body", "body_raw_ref"])
|
self._check_at_least_one_property(["body", "body_raw_ref"])
|
||||||
|
|
||||||
|
|
||||||
class EmailMessage(Observable):
|
class EmailMessage(_Observable):
|
||||||
_type = 'email-message'
|
_type = 'email-message'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -111,7 +111,7 @@ class EmailMessage(Observable):
|
||||||
# self._dependency(["is_multipart"], ["body"], [False])
|
# self._dependency(["is_multipart"], ["body"], [False])
|
||||||
|
|
||||||
|
|
||||||
class File(Observable):
|
class File(_Observable):
|
||||||
_type = 'file'
|
_type = 'file'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -140,7 +140,7 @@ class File(Observable):
|
||||||
self._check_at_least_one_property(["hashes", "name"])
|
self._check_at_least_one_property(["hashes", "name"])
|
||||||
|
|
||||||
|
|
||||||
class IPv4Address(Observable):
|
class IPv4Address(_Observable):
|
||||||
_type = 'ipv4-addr'
|
_type = 'ipv4-addr'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -150,7 +150,7 @@ class IPv4Address(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class IPv6Address(Observable):
|
class IPv6Address(_Observable):
|
||||||
_type = 'ipv6-addr'
|
_type = 'ipv6-addr'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -160,7 +160,7 @@ class IPv6Address(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class MACAddress(Observable):
|
class MACAddress(_Observable):
|
||||||
_type = 'mac-addr'
|
_type = 'mac-addr'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -168,7 +168,7 @@ class MACAddress(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Mutex(Observable):
|
class Mutex(_Observable):
|
||||||
_type = 'mutex'
|
_type = 'mutex'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -176,7 +176,7 @@ class Mutex(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class NetworkTraffic(Observable):
|
class NetworkTraffic(_Observable):
|
||||||
_type = 'network-traffic'
|
_type = 'network-traffic'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -205,7 +205,7 @@ class NetworkTraffic(Observable):
|
||||||
self._check_at_least_one_property(["src_ref", "dst_ref"])
|
self._check_at_least_one_property(["src_ref", "dst_ref"])
|
||||||
|
|
||||||
|
|
||||||
class Process(Observable):
|
class Process(_Observable):
|
||||||
_type = 'process'
|
_type = 'process'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -227,7 +227,7 @@ class Process(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Software(Observable):
|
class Software(_Observable):
|
||||||
_type = 'software'
|
_type = 'software'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -239,7 +239,7 @@ class Software(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class URL(Observable):
|
class URL(_Observable):
|
||||||
_type = 'url'
|
_type = 'url'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -247,7 +247,7 @@ class URL(Observable):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserAccount(Observable):
|
class UserAccount(_Observable):
|
||||||
_type = 'user-account'
|
_type = 'user-account'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -273,11 +273,25 @@ class WindowsRegistryValueType(_STIXBase):
|
||||||
_properties = {
|
_properties = {
|
||||||
'name': StringProperty(required=True),
|
'name': StringProperty(required=True),
|
||||||
'data': StringProperty(),
|
'data': StringProperty(),
|
||||||
'data_type': Property()
|
'data_type': EnumProperty([
|
||||||
|
'REG_NONE',
|
||||||
|
'REG_SZ',
|
||||||
|
'REG_EXPAND_SZ',
|
||||||
|
'REG_BINARY',
|
||||||
|
'REG_DWORD',
|
||||||
|
'REG_DWORD_BIG_ENDIAN',
|
||||||
|
'REG_LINK',
|
||||||
|
'REG_MULTI_SZ',
|
||||||
|
'REG_RESOURCE_LIST',
|
||||||
|
'REG_FULL_RESOURCE_DESCRIPTION',
|
||||||
|
'REG_RESOURCE_REQUIREMENTS_LIST',
|
||||||
|
'REG_QWORD',
|
||||||
|
'REG_INVALID_TYPE',
|
||||||
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class WindowsRegistryKey(Observable):
|
class WindowsRegistryKey(_Observable):
|
||||||
_type = 'windows-registry-key'
|
_type = 'windows-registry-key'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
@ -291,8 +305,9 @@ class WindowsRegistryKey(Observable):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def values(self):
|
def values(self):
|
||||||
|
# Needed because 'values' is a property on collections.Mapping objects
|
||||||
return self._inner['values']
|
return self._inner['values']
|
||||||
|
|
||||||
|
|
||||||
class X509V3ExtenstionsType(_STIXBase):
|
class X509V3ExtenstionsType(_STIXBase):
|
||||||
_type = 'x509-v3-extensions-type'
|
_type = 'x509-v3-extensions-type'
|
||||||
|
@ -314,9 +329,9 @@ class X509V3ExtenstionsType(_STIXBase):
|
||||||
'certificate_policies': StringProperty(),
|
'certificate_policies': StringProperty(),
|
||||||
'policy_mappings': StringProperty(),
|
'policy_mappings': StringProperty(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class X509Certificate(_Observable):
|
||||||
class X509Certificate(Observable):
|
|
||||||
_type = 'x509-certificate'
|
_type = 'x509-certificate'
|
||||||
_properties = {
|
_properties = {
|
||||||
'type': TypeProperty(_type),
|
'type': TypeProperty(_type),
|
||||||
|
|
|
@ -5,10 +5,12 @@ import datetime as dt
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from six import text_type
|
|
||||||
import pytz
|
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
from .base import Observable, _STIXBase
|
import pytz
|
||||||
|
from six import text_type
|
||||||
|
|
||||||
|
from .base import _Observable, _STIXBase
|
||||||
from .exceptions import DictionaryKeyError
|
from .exceptions import DictionaryKeyError
|
||||||
|
|
||||||
|
|
||||||
|
@ -141,6 +143,7 @@ class StringProperty(Property):
|
||||||
|
|
||||||
|
|
||||||
class TypeProperty(Property):
|
class TypeProperty(Property):
|
||||||
|
|
||||||
def __init__(self, type):
|
def __init__(self, type):
|
||||||
super(TypeProperty, self).__init__(fixed=type)
|
super(TypeProperty, self).__init__(fixed=type)
|
||||||
|
|
||||||
|
@ -226,7 +229,7 @@ class ObservableProperty(Property):
|
||||||
from .__init__ import parse_observable # avoid circular import
|
from .__init__ import parse_observable # avoid circular import
|
||||||
for key, obj in dictified.items():
|
for key, obj in dictified.items():
|
||||||
parsed_obj = parse_observable(obj, dictified.keys())
|
parsed_obj = parse_observable(obj, dictified.keys())
|
||||||
if not issubclass(type(parsed_obj), Observable):
|
if not issubclass(type(parsed_obj), _Observable):
|
||||||
raise ValueError("Objects in an observable property must be "
|
raise ValueError("Objects in an observable property must be "
|
||||||
"Cyber Observable Objects")
|
"Cyber Observable Objects")
|
||||||
dictified[key] = parsed_obj
|
dictified[key] = parsed_obj
|
||||||
|
@ -308,6 +311,7 @@ REF_REGEX = re.compile("^[a-z][a-z-]+[a-z]--[0-9a-fA-F]{8}-[0-9a-fA-F]{4}"
|
||||||
|
|
||||||
|
|
||||||
class ReferenceProperty(Property):
|
class ReferenceProperty(Property):
|
||||||
|
|
||||||
def __init__(self, required=False, type=None):
|
def __init__(self, required=False, type=None):
|
||||||
"""
|
"""
|
||||||
references sometimes must be to a specific object type
|
references sometimes must be to a specific object type
|
||||||
|
@ -330,6 +334,7 @@ SELECTOR_REGEX = re.compile("^[a-z0-9_-]{3,250}(\\.(\\[\\d+\\]|[a-z0-9_-]{1,250}
|
||||||
|
|
||||||
|
|
||||||
class SelectorProperty(Property):
|
class SelectorProperty(Property):
|
||||||
|
|
||||||
def __init__(self, type=None):
|
def __init__(self, type=None):
|
||||||
# ignore type
|
# ignore type
|
||||||
super(SelectorProperty, self).__init__()
|
super(SelectorProperty, self).__init__()
|
||||||
|
@ -345,6 +350,7 @@ class ObjectReferenceProperty(StringProperty):
|
||||||
|
|
||||||
|
|
||||||
class EmbeddedObjectProperty(Property):
|
class EmbeddedObjectProperty(Property):
|
||||||
|
|
||||||
def __init__(self, type, required=False):
|
def __init__(self, type, required=False):
|
||||||
self.type = type
|
self.type = type
|
||||||
super(EmbeddedObjectProperty, self).__init__(required, type=type)
|
super(EmbeddedObjectProperty, self).__init__(required, type=type)
|
||||||
|
@ -355,3 +361,18 @@ class EmbeddedObjectProperty(Property):
|
||||||
elif not isinstance(value, self.type):
|
elif not isinstance(value, self.type):
|
||||||
raise ValueError("must be of type %s." % self.type.__name__)
|
raise ValueError("must be of type %s." % self.type.__name__)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class EnumProperty(StringProperty):
|
||||||
|
|
||||||
|
def __init__(self, allowed, **kwargs):
|
||||||
|
if type(allowed) is not list:
|
||||||
|
allowed = list(allowed)
|
||||||
|
self.allowed = allowed
|
||||||
|
super(EnumProperty, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
value = super(EnumProperty, self).clean(value)
|
||||||
|
if value not in self.allowed:
|
||||||
|
raise ValueError("value '%s' is not valid for this enumeration." % value)
|
||||||
|
return self.string_type(value)
|
||||||
|
|
|
@ -4,8 +4,8 @@ from .base import _STIXBase
|
||||||
from .common import COMMON_PROPERTIES
|
from .common import COMMON_PROPERTIES
|
||||||
from .other import KillChainPhase
|
from .other import KillChainPhase
|
||||||
from .properties import (IDProperty, IntegerProperty, ListProperty,
|
from .properties import (IDProperty, IntegerProperty, ListProperty,
|
||||||
ObservableProperty, ReferenceProperty,
|
ObservableProperty, ReferenceProperty, StringProperty,
|
||||||
StringProperty, TimestampProperty, TypeProperty)
|
TimestampProperty, TypeProperty)
|
||||||
from .utils import NOW
|
from .utils import NOW
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -587,7 +587,7 @@ def test_software_example():
|
||||||
assert s.version == "2002"
|
assert s.version == "2002"
|
||||||
assert s.vendor == "Microsoft"
|
assert s.vendor == "Microsoft"
|
||||||
|
|
||||||
|
|
||||||
def test_url_example():
|
def test_url_example():
|
||||||
s = stix2.URL(value="https://example.com/research/index.html")
|
s = stix2.URL(value="https://example.com/research/index.html")
|
||||||
|
|
||||||
|
@ -619,26 +619,23 @@ def test_user_account_example():
|
||||||
assert a.password_last_changed == dt.datetime(2016, 1, 20, 14, 27, 43, tzinfo=pytz.utc)
|
assert a.password_last_changed == dt.datetime(2016, 1, 20, 14, 27, 43, tzinfo=pytz.utc)
|
||||||
assert a.account_first_login == dt.datetime(2016, 1, 20, 14, 26, 7, tzinfo=pytz.utc)
|
assert a.account_first_login == dt.datetime(2016, 1, 20, 14, 26, 7, tzinfo=pytz.utc)
|
||||||
assert a.account_last_login == dt.datetime(2016, 7, 22, 16, 8, 28, tzinfo=pytz.utc)
|
assert a.account_last_login == dt.datetime(2016, 7, 22, 16, 8, 28, tzinfo=pytz.utc)
|
||||||
|
|
||||||
|
|
||||||
def test_windows_registry_key_example():
|
def test_windows_registry_key_example():
|
||||||
rk = stix2.WindowsRegistryKey(key="hkey_local_machine\\system\\bar\\foo",
|
with pytest.raises(ValueError):
|
||||||
values=[{
|
v = stix2.WindowsRegistryValueType(name="Foo",
|
||||||
"name": "Foo",
|
data="qwerty",
|
||||||
"data": "qwerty",
|
data_type="string")
|
||||||
"data_type": "REG_SZ"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Bar",
|
|
||||||
"data": "42",
|
|
||||||
"data_type": "REG_DWORD"
|
|
||||||
}])
|
|
||||||
|
|
||||||
assert rk.type == "windows-registry-key"
|
v = stix2.WindowsRegistryValueType(name="Foo",
|
||||||
assert rk.key == "hkey_local_machine\\system\\bar\\foo"
|
data="qwerty",
|
||||||
assert rk.values[0].name == "Foo"
|
data_type="REG_SZ")
|
||||||
assert rk.values[0].data == "qwerty"
|
w = stix2.WindowsRegistryKey(key="hkey_local_machine\\system\\bar\\foo",
|
||||||
assert rk.values[0].data_type == "REG_SZ"
|
values=[v])
|
||||||
|
assert w.key == "hkey_local_machine\\system\\bar\\foo"
|
||||||
|
assert w.values[0].name == "Foo"
|
||||||
|
assert w.values[0].data == "qwerty"
|
||||||
|
assert w.values[0].data_type == "REG_SZ"
|
||||||
|
|
||||||
|
|
||||||
def test_x509_certificate_example():
|
def test_x509_certificate_example():
|
||||||
|
@ -651,3 +648,4 @@ def test_x509_certificate_example():
|
||||||
assert x509.type == "x509-certificate"
|
assert x509.type == "x509-certificate"
|
||||||
assert x509.issuer == "C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com" # noqa
|
assert x509.issuer == "C=ZA, ST=Western Cape, L=Cape Town, O=Thawte Consulting cc, OU=Certification Services Division, CN=Thawte Server CA/emailAddress=server-certs@thawte.com" # noqa
|
||||||
assert x509.subject == "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org" # noqa
|
assert x509.subject == "C=US, ST=Maryland, L=Pasadena, O=Brent Baccala, OU=FreeSoft, CN=www.freesoft.org/emailAddress=baccala@freesoft.org" # noqa
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ from stix2.exceptions import DictionaryKeyError
|
||||||
from stix2.observables import EmailMIMEComponent
|
from stix2.observables import EmailMIMEComponent
|
||||||
from stix2.properties import (BinaryProperty, BooleanProperty,
|
from stix2.properties import (BinaryProperty, BooleanProperty,
|
||||||
DictionaryProperty, EmbeddedObjectProperty,
|
DictionaryProperty, EmbeddedObjectProperty,
|
||||||
HashesProperty, HexProperty, IDProperty,
|
EnumProperty, HashesProperty, HexProperty,
|
||||||
IntegerProperty, ListProperty, Property,
|
IDProperty, IntegerProperty, ListProperty,
|
||||||
ReferenceProperty, StringProperty,
|
Property, ReferenceProperty, StringProperty,
|
||||||
TimestampProperty, TypeProperty)
|
TimestampProperty, TypeProperty)
|
||||||
|
|
||||||
from .constants import FAKE_TIME
|
from .constants import FAKE_TIME
|
||||||
|
@ -247,3 +247,11 @@ def test_embedded_property():
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
emb_prop.clean("string")
|
emb_prop.clean("string")
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_property():
|
||||||
|
enum_prop = EnumProperty(['a', 'b', 'c'])
|
||||||
|
assert enum_prop.clean('b')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
enum_prop.clean('z')
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import pytz
|
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
import pytz
|
||||||
|
|
||||||
# Sentinel value for fields that should be set to the current time.
|
# Sentinel value for fields that should be set to the current time.
|
||||||
# We can't use the standard 'default' approach, since if there are multiple
|
# We can't use the standard 'default' approach, since if there are multiple
|
||||||
|
|
Loading…
Reference in New Issue