mvantellingen / python-zeep

A Python SOAP client
http://docs.python-zeep.org
Other
1.9k stars 586 forks source link

ISO 8601 time designator 'T' missing. Unable to parse datetime string '2017-10-18' #672

Closed ismettaslann closed 6 years ago

ismettaslann commented 6 years ago

Hi, I'm trying to send soap request with wsdl, I'am getting response but somehow all datetime parameters return 'None'. I'am using zeep version 2.5.0.

Part of wsdl, actually xsd is below, this is the type that return 'None';

<xs:element name="DueDate" minOccurs="1" maxOccurs="1" nillable="true">
<xs:simpleType>
<xs:restriction base="xs:dateTime"/>
</xs:simpleType>
</xs:element>

This is my response data;

{
    'AccountID': '1234-1234-123456',
    'Title': 'Mr.Ahmad',
    'Status': 'S',
    'MessageText': 'Transaction exists',
    'Bills': {
        'Bill': [
            {
                'BillID': '123123123123',
                'LPSAmount': Decimal('100'),
                'Amount': Decimal('400'),
                'TotalAmount': Decimal('500'),
                'DueDate': None,
                'BillType': 'Electric',
                'SerialNumber': '1234',
                'BillCreation': None
            }
        ]
    }
}

And this is my script;

 try:
        binding_name, address = get_point_of_bill_inquiry()
        client = zeep.Client(wsdl=address, strict=False)
        srv = client.create_service(binding_name=binding_name, address=address)
        response = srv.BillInquiryRequestOperation(account_id,user_id, password)
        print(response)
        return jsonify(response)
    except Exception as e:
        print(e)

And this is error;

File "D:\Users\user-name\AppData\Local\Programs\Python\Python36-32\lib\site-packages\isodate\isodatetime.py", line 51, in parse_datetime
    datestring, timestring = datetimestring.split('T')
ValueError: not enough values to unpack (expected 2, got 1)
Traceback (most recent call last):
  File "D:\Users\user-name\AppData\Local\Programs\Python\Python36-32\lib\site-packages\zeep\xsd\types\simple.py", line 61, in parse_xmlelement
    return self.pythonvalue(xmlelement.text)
  File "D:\Users\user-name\AppData\Local\Programs\Python\Python36-32\lib\site-packages\zeep\xsd\types\builtins.py", line 141, in pythonvalue
    return isodate.parse_datetime(value)
File "D:\Users\user-name\AppData\Local\Programs\Python\Python36-32\lib\site-packages\isodate\isodatetime.py", line 54, in parse_datetime
    " parse datetime string %r" % datetimestring)
isodate.isoerror.ISO8601Error: ISO 8601 time designator 'T' missing. Unable to parse datetime string '2017-10-18'

Thank you!

jean152 commented 6 years ago

I believe you have this error because the "datetime" is not in the isoformat ISO 8601. And maybe since it is not in this format, it returns None ?

Do you know how is the expected data ? It should be in the ISO 8601 format: like this:

2018-01-29T13:00:00 Notice the "T" between the date and time.

Maybe take a look here: https://docs.python.org/3/library/datetime.html?highlight=datetime%20isoformat#datetime.datetime.isoformat

ismettaslann commented 6 years ago

Yes, My return data isn't datetime iso format. But, actually i can't intefere in casting to iso8601. I mean, is respone has to return iso8601 format? My actual question is, what can i do when response return like '27-01-2017' ? Because zeep does not let me accept like string or etc.

sighthon commented 6 years ago

@ismettaslann I faced a similar problem in one of my projects. We came up with two solutions: 1) Convert the date to ISO before zeep parses it: This can be done by modifying the function (file : zeep\xsd\types\builtins.py class : Date method : pythonvalue() ) to -

    def pythonvalue(self, value):
        try:
            _date = datetime.strptime(value, "%d-%m-%Y").date()
            value = str(_date)
        except:
            pass

        return isodate.parse_date(value)

2) Write a plugin to parse the response yourself and change the dates to ISO format (Not implemented yet)

The first one is a quick and nasty solution whereas second option seems to be more legit. Hope this helps.

shausman commented 6 years ago

I had this same issue @asutkarpeeyush 's fix didnt work for my date issue. I did a really lazy 'fix' which doesn't fix any typing issues but works for my specific application

def pythonvalue(self, value):
        if 'T' not in value:
            value = value + 'T00:00:00'
        return isodate.parse_datetime(value)
ismettaslann commented 6 years ago

That is really good solution. But I fix my problem with different way. Thank you for your response :)

sighthon commented 6 years ago

@shausman yea coz your date format is different from what @ismettaslann had asked for! @ismettaslann I would love to know how you have fixed it :)

ismettaslann commented 6 years ago

I forgot to write my solution, sorry :) Actually my solution isn't the perfect way, But it works :) I imported to xmltodict library and it converted all data succesfully.

response_from_xml = xmltodict.parse(response.text)

data=dict(response_from_xml.get("env:Envelope").get("env:Body").get("AddCustomerAutopayResponse"))

return jsonify(data)

I used this like that as above.

sighthon commented 6 years ago

@ismettaslann @shausman I ended up coding the second solution I had proposed. Posting it for anyone who might need it someday.

class _XMLParsingPlugin(Plugin):
    def ingress(self, envelope, http_headers, operation):

        # define the namespace
        namespace = {'soap': 'http://schemas.xmlsoap.org/soap/envelope/'}

        # start iterating over the xml from body tag
        xml_elements = Queue()
        body = envelope.find("soap:Body", namespace)
        xml_elements.put(body)

        while not xml_elements.empty():
            element = xml_elements.get()

            # if element's text is a date ("%d-%m-%Y"), change the format
            # can have multiple operations here!!
            try:
                element_text = element.text
                _date = datetime.strptime(element_text, "%d-%b-%Y").date()
                element.text = str(_date)
            except:
                pass

            # push all child elements to the queue
            element_children = list(element)
            for child in element_children:
                try:
                    xml_elements.put(child, block=False)
                except Full:
                    raise Full("Queue is full")

        return envelope, http_headers
mvantellingen commented 6 years ago

Fixed in master, with a bit of a hack :-)

TitanFighter commented 4 years ago

In my case the error was: isodate.isoerror.ISO8601Error: Unable to parse duration string '55', issued by return isodate.parse_duration(value) (source code in Zeep)

Checked source code of isodate. In description all examples start with P -> In wiki found what is P:

P is the duration designator (for period) placed at the start of the duration representation.

and an example:

"P3Y6M4DT12H30M5S" represents a duration of "three years, six months, four days, twelve hours, thirty minutes, and five seconds".

Full code:

import zeep

class HandleDurationTime(zeep.Plugin):
    """
    Fix the error: "isodate.isoerror.ISO8601Error: Unable to parse duration string '55'"
    "Duration" must have the next format 'P3Y6M4DT12H30M5S', which represents
    a duration of "three years, six months, four days, twelve hours, thirty minutes, and five seconds"
    """

    def ingress(self, envelope, http_headers, operation):
        tree = envelope

        # "Flight" is a tag name
        for i in tree.xpath("//*[name()='Flight']"):
            # "Duration" is an attribute of "Flight" tag, ie:
            # <Flight DepartureDateTime="2019-12-31T09:15:00" TravelCode="100109" Duration="55">...</Flight>
            duration = i.get('Duration')

            if duration and duration.isdigit():
                minutes = int(duration)
                hours, minutes = minutes // 60, minutes % 60

                if minutes > 59:
                    raise NotImplementedError('Please convert minutes to hours')

                # "P3Y6M4DT12H30M5S" .
                date_time_str = f'P0Y0M0DT{hours}H{minutes}M0S'

                i.set('Duration', date_time_str)

        return envelope, http_headers

def run():
    settings = zeep.Settings(strict=True)
    client = zeep.Client(wsdl='path_to_wsdl_file', settings=settings, plugins=[HandleDurationTime()])

    some_data_to_send = {}

    r = client.service.SERVICE_NAME(**some_data_to_send)
Alexander-Fedorovtsev commented 1 year ago

Hi, plugin is great solution. I have same error and my response with data string is:

2023-02-14 13:13:26.240

Zeep cant parse it, because T is missing in datetime.

Alexander-Fedorovtsev commented 1 year ago

And my short solution for this

class _DateToISOPlugin(Plugin):
    def ingress(self, envelope, http_headers, operation):
        tree = envelope
        for i in tree.xpath("//*[name()='date']"):
            i.text = i.text.replace(' ', 'T')
        return envelope, http_headers