Leave the SimpleIO.input_optional out of the input


#1

Hi,

I am using SimpleIO.input_optional parameters. When my service is called without these parameters they get a default value (seems to be u’’). It would be nice if it is possible to leave these parameters out of of self.request.input when the parameters are not used in the request. Because now it is impossible to discriminate between there two situations:

  • parameters was not in the input and gets default value
  • parameter was in the input, and the value is the same as the default value

Even nicer would be to make this behaviour configurable.

Regards, Jan


#2

Hi @jjmurre,

yes, it makes sense to make it configurable - can you please open a ticket in GH?

As for default values - please use ‘default_value’, as below. I.e. default value is an empty string only if that attribute is not set.

class SimpleIO:
    input_optional = ('foo',)
    default_value = 'UNKNOWN'

Regards.


#3

Hi @dsuch,

Thanks for the suggestion about the default values. I have created a github issues about the mentioned configurability.

Regards, Jan


#4

one thing i’ve done is set default_value to something that’s not appropriate for any of the inputs, e.g., default_value="JUSTINBIEBER"

then in the handle method one can do something like

if self.input.artist=="JUSTINBIEBER": 
    # handle input not present
else:
    # handle input present
    

#5

Just catching up with this thread, as I am having the same issue:

class Create(Service):
    """Service class to create a new login."""

    class SimpleIO:
        input_required = ('username', AsIs('password'))
        input_optional = ('name', 'surname', 'email', Boolean('is_admin'))
        output_required = ('id', 'username')
        output_optional = ('password', 'name', 'surname', 'email', 'is_admin')
        skip_empty_keys = True

    def handle(self):
        conn = self.user_config.genesisng.database.connection

        p = self.request.input
        login = Login(
            username=p.username,
            password=p.password,
            name=p.name,
            surname=p.surname,
            email=p.email,
            is_admin=p.is_admin)
        # If is_admin is not passed as parameter, we need to transform
        # the empty string u'' into False
        login.is_admin = True if login.is_admin else False

        [..]

skip_empty_keys works fine with the output, but I am still manually correcting is_admin, as it arrives as u’’ when it is not passed as parameter. Using default_value is not an option for me, as the default value None is required for the other, non-boolean optional parameters.

How are you guys dealing with it these days?


#6

I have just made it possible to specify default values on a per-element basis, so you can now use:

class SimpleIO:
    input_required = 'user_id',
    input_optional = ('user_name', Bool('is_admin', default=False))

#7

Working flawlessly. I tried:

  • Without passing is_admin.
  • Passing is_admin as true.
  • Passing is_admin as false.

They all worked fine. Cheers, @dsuch! :slight_smile:


#8

FYI, my previous reply was tested when creating a new record. When updating a record, to allow partial updates (i.e. all fields are optional) I have come up with this solution:

class Update(Service):
    """Service class to update an existing login."""
    """Channel /genesisng/logins/{id}/update."""

    class SimpleIO:
        input_required = (Integer('id'),)
        input_optional = ('username', AsIs('password'), 'name', 'surname',
                          'email', Boolean('is_admin'))
        output_required = ('id', 'username')
        output_optional = ('password', 'name', 'surname', 'email', 'is_admin')
        skip_empty_keys = True

    def handle(self):
        conn = self.user_config.genesisng.database.connection
        id_ = self.request.input.id
        p = self.request.input

        with closing(self.outgoing.sql.get(conn).session()) as session:
            result = session.query(Login).filter(Login.id == id_).one_or_none()

            if result:
                # Update dictionary keys
                if p.username:
                    result.username = p.username
                if p.password:
                    result.password = p.password
                if p.name:
                    result.name = p.name
                if p.surname:
                    result.surname = p.surname
                if p.email:
                    result.email = p.email
                if p.is_admin != '':
                    result.is_admin = p.is_admin
                session.commit()
                self.response.status_code = OK
                self.response.payload = result
            else:
                self.response.status_code = NOT_FOUND
                self.response.payload = ''

Does that sound like a correct/optimal approach to you?


#9

Can you tell me what self.logger.info('Type is %r', type(result)) outputs? I would like to check what the actual Python type this is. Thanks.


#10

Added the call to self.logger.info right after executing:

result = session.query(Login).filter(Login.id == id_).one_or_none()

And this was the output in the logs:

2018-10-11 07:56:57,485 DEBG 'zato-server2' stdout output:
2018-10-11 07:56:57,484 - INFO - 145:DummyThread-7 - login.update:154 - Type is <class 'genesisng.schema.login.Login'>

FYI, this is the schema class:

# coding: utf8
from __future__ import absolute_import, division
from __future__ import print_function, unicode_literals
from .base import Base
from sqlalchemy import Column, Boolean, Integer, String, Index


class Login(Base):
    __tablename__ = 'login'
    __rels__ = []
    __table_args__ = (
        # Trigram GIN indexes for searches (using ILIKE)
        Index('ix_trgm_login_username', 'username', postgresql_using='gin',
              postgresql_ops={'username': 'gin_trgm_ops'}),
        Index('ix_trgm_login_name', 'name', postgresql_using='gin',
              postgresql_ops={'name': 'gin_trgm_ops'}),
        Index('ix_trgm_login_surname', 'surname', postgresql_using='gin',
              postgresql_ops={'surname': 'gin_trgm_ops'}),
    )

    id = Column(Integer, primary_key=True)
    username = Column(String(20), index=True, unique=True, nullable=False)
    password = Column(String(255), nullable=False)
    name = Column(String(50), index=True)
    surname = Column(String(50), index=True)
    email = Column(String(255), index=True, unique=True)
    is_admin = Column(Boolean, default=False)

    def __repr__(self):
        return "<Login(id='%s', username='%s', name='%s', surname='%s', email='%s')>" % (
            self.id, self.username, self.name, self.surname, self.email)

#11

@dsuch, when executing zato enmasse I am not getting errors regarding Booleans not being iterable. Executing:

zato@kasumi:~$ zato enmasse /opt/zato/env/qs-1/server1/ --export-odb --verbose
could not fetch service list
Type channel_amqp has no 'get-list' service
Type web_socket has no 'get-list' service
Type pubsub_endpoint has no 'get-list' service
Type channel_jms_wmq has no 'get-list' service
Type channel_zmq has no 'get-list' service
Type def_sec has no 'get-list' service
Type http_soap has no 'get-list' service
Type scheduler has no 'get-list' service
Type notif_sql has no 'get-list' service
Type outconn_amqp has no 'get-list' service
Type outconn_jms_wmq has no 'get-list' service
Type query_cassandra has no 'get-list' service
ODB objects read
ODB objects merged in
Data exported to /opt/zato/zato-export-2018-10-11T08_16_05_586515.yml

Produces these in the logs:

2018-10-11 08:13:58,182 DEBG 'zato-server1' stdout output:
2018-10-11 08:13:58,181 - WARNING - 117:DummyThread-47 - zato.server.service:501 - Traceback (most recent call last):
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 476, in update_handle
    self._invoke(service, channel)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 398, in _invoke
    service.handle()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/internal/apispec/__init__.py", line 68, in handle
    include, exclude, self.request.input.query).get_info()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 261, in get_info
    self.parse()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 342, in parse
    info = ServiceInfo(details.name, details.service_class, self.simple_io_config)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 90, in __init__
    self.parse()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 95, in parse
    self.set_config()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 182, in set_config
    self._add_ns_sio()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 150, in _add_ns_sio
    for param in param_list:
TypeError: 'Boolean' object is not iterable


2018-10-11 08:13:58,182 DEBG 'zato-server1' stdout output:
2018-10-11 08:13:58,182 - WARNING - 117:DummyThread-47 - zato.server.service:585 - Could not invoke `zato.apispec.get-api-spec`, e:`Traceback (most recent call last):
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 583, in invoke_by_impl_name
    return self.update_handle(*invoke_args, **kwargs)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 476, in update_handle
    self._invoke(service, channel)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 398, in _invoke
    service.handle()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/internal/apispec/__init__.py", line 68, in handle
    include, exclude, self.request.input.query).get_info()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 261, in get_info
    self.parse()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 342, in parse
    info = ServiceInfo(details.name, details.service_class, self.simple_io_config)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 90, in __init__
    self.parse()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 95, in parse
    self.set_config()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 182, in set_config
    self._add_ns_sio()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 150, in _add_ns_sio
    for param in param_list:
TypeError: 'Boolean' object is not iterable
`

2018-10-11 08:13:58,182 DEBG 'zato-server1' stdout output:
2018-10-11 08:13:58,182 - WARNING - 117:DummyThread-47 - zato.server.service:501 - Traceback (most recent call last):
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 476, in update_handle
    self._invoke(service, channel)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 398, in _invoke
    service.handle()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/internal/service.py", line 311, in handle
    response = func(id_, payload, channel, data_format, transport, serialize=True)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 601, in invoke
    return self.invoke_by_impl_name(self.server.service_store.name_to_impl_name[name], *args, **kwargs)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 583, in invoke_by_impl_name
    return self.update_handle(*invoke_args, **kwargs)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 476, in update_handle
    self._invoke(service, channel)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 398, in _invoke
    service.handle()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/internal/apispec/__init__.py", line 68, in handle
    include, exclude, self.request.input.query).get_info()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 261, in get_info
    self.parse()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 342, in parse
    info = ServiceInfo(details.name, details.service_class, self.simple_io_config)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 90, in __init__
    self.parse()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 95, in parse
    self.set_config()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 182, in set_config
    self._add_ns_sio()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 150, in _add_ns_sio
    for param in param_list:
TypeError: 'Boolean' object is not iterable


2018-10-11 08:13:58,183 DEBG 'zato-server1' stdout output:
2018-10-11 08:13:58,183 - ERROR - 117:DummyThread-47 - zato.server.connection.http_soap.channel:324 - Caught an exception, cid:`bf413d6ff5fd6b63111318fa`, status_code:`500`, _format_exc:`Traceback (most recent call last):
  File "/opt/zato/3.0/code/zato-server/src/zato/server/connection/http_soap/channel.py", line 268, in dispatch
    payload, worker_store, self.simple_io_config, post_data, path_info, soap_action)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/connection/http_soap/channel.py", line 502, in handle
    params_priority=channel_item.params_pri)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 476, in update_handle
    self._invoke(service, channel)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 398, in _invoke
    service.handle()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/internal/service.py", line 311, in handle
    response = func(id_, payload, channel, data_format, transport, serialize=True)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 601, in invoke
    return self.invoke_by_impl_name(self.server.service_store.name_to_impl_name[name], *args, **kwargs)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 583, in invoke_by_impl_name
    return self.update_handle(*invoke_args, **kwargs)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 476, in update_handle
    self._invoke(service, channel)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/__init__.py", line 398, in _invoke
    service.handle()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/service/internal/apispec/__init__.py", line 68, in handle
    include, exclude, self.request.input.query).get_info()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 261, in get_info
    self.parse()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 342, in parse
    info = ServiceInfo(details.name, details.service_class, self.simple_io_config)
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 90, in __init__
    self.parse()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 95, in parse
    self.set_config()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 182, in set_config
    self._add_ns_sio()
  File "/opt/zato/3.0/code/zato-server/src/zato/server/apispec/__init__.py", line 150, in _add_ns_sio
    for param in param_list:
TypeError: 'Boolean' object is not iterable

At the moment I have only configured channels for the Login module, so it has to be down to it.

Hot-deploying the login module does not return any errors:

docker cp genesisng/services/login.py 511a3594b3dc:/opt/zato/env/qs-1/server1/pickup/incoming/services/

Produces:

2018-10-11 08:17:50,331 DEBG 'zato-server2' stdout output:
2018-10-11 08:17:50,331 - INFO - 145:DummyThread-39 - zato.hot-deploy.create:376 - Creating tar archive

2018-10-11 08:17:50,331 DEBG 'zato-server1' stdout output:
2018-10-11 08:17:50,331 - INFO - 117:DummyThread-57 - zato.hot-deploy.create:376 - Creating tar archive

2018-10-11 08:17:50,353 DEBG 'zato-server1' stdout output:
2018-10-11 08:17:50,353 - INFO - 117:DummyThread-57 - zato.hot-deploy.create:376 - Creating tar archive

2018-10-11 08:17:50,354 DEBG 'zato-server2' stdout output:
2018-10-11 08:17:50,354 - INFO - 145:DummyThread-39 - zato.hot-deploy.create:376 - Creating tar archive

2018-10-11 08:17:50,761 DEBG 'zato-server2' stdout output:
2018-10-11 08:17:50,761 - INFO - 145:DummyThread-39 - zato.hot-deploy.create:156 - Uploaded package id:`82`, payload_name:`login.py`

2018-10-11 08:17:50,822 DEBG 'zato-server1' stdout output:
2018-10-11 08:17:50,822 - INFO - 117:DummyThread-57 - zato.hot-deploy.create:156 - Uploaded package id:`82`, payload_name:`login.py`

I can call the list service without problems:

curl -v -g "http://127.0.0.1:11223/genesisng/logins/list?page=1&size=50&sort_by=name&order_by=desc&fields=id&fields=name&filters=id|gt|50"; echo ""
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 11223 (#0)
> GET /genesisng/logins/list?page=1&size=50&sort_by=name&order_by=desc&fields=id&fields=name&filters=id|gt|50 HTTP/1.1
> Host: 127.0.0.1:11223
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: Zato
< Date: Thu, 11 Oct 2018 08:19:00 GMT
< Connection: close
< Transfer-Encoding: chunked
< Content-Type: application/json
< X-Zato-CID: 443f8f8f2c01d4005f9ee482
< 
{"response": [{"count": 952, "id": 51, "name": "Karyn"}, {"count": 952, "id": 52, "name": "Sigourney"}, {"count": 952, "id": 53, "name": "Berk"}, {"count": 952, "id": 54, "name": "Judah"}, {"count": 952, "id": 55, "name": "Tyler"}, {"count": 952, "id": 56, "name": "Emery"}, {"count": 952, "id": 57, "name": "Buckminster"}, {"count": 952, "id": 58, "name": "Byron"}, {"count": 952, "id": 59, "name": "Todd"}, {"count": 952, "id": 60, "name": "Candice"}, {"count": 952, "id": 61, "name": "Maia"}, {"count": 952, "id": 62, "name": "Luke"}, {"count": 952, "id": 63, "name": "Blaze"}, {"count": 952, "id": 64, "name": "Ira"}, {"count": 952, "id": 65, "name": "Brady"}, {"count": 952, "id": 66, "name": "Reese"}, {"count": 952, "id": 67, "name": "Vernon"}, {"count": 952, "id": 68, "name": "Tatum"}, {"count": 952, "id": 69, "name": "Jana"}, {"count": 952, "id": 70, "name": "Kadeem"}, {"count": 952, "id": 71, "name": "Jerome"}, {"count": 952, "id": 72, "name": "Drake"}, {"count": 952, "id": 73, "name": "Iliana"}, {"count": 952, "id": 74, "name": "Isabella"}, {"count": 952, "id": 75, "name": "Maite"}, {"count": 952, "id": 76, "name": "Isadora"}, {"count": 952, "id": 77, "name": "Brett"}, {"count": 952, "id": 78, "name": "Tatum"}, {"count": 952, "id": 79, "name": "Melvin"}, {"count": 952, "id": 80, "name": "Zenaida"}, {"count": 952, "id": 81, "name": "Kelly"}, {"count": 952, "id": 82, "name": "Quon"}, {"count": 952, "id": 83, "name": "Matthew"}, {"count": 952, "id": 84, "name": "Gwendolyn"}, {"count": 952, "id": 85, "name": "Adria"}, {"count": 952, "id": 86, "name": "Judah"}, {"count": 952, "id": 87, "name": "Keaton"}, {"count": 952, "id": 88, "name": "Shelly"}, {"count": 952, "id": 89, "name": "Maggie"}, {"count": 952, "id": 90, "name": "Helen"}, {"count": 952, "id": 91, "name": "Alec"}, {"count": 952, "id": 92, "name": "Graiden"}, {"count": 952, "id": 93, "name": "Lucian"}, {"count": 952, "id": 94, "name": "Bevis"}, {"count": 952, "id": 95, "name": "Elliott"}, {"count": 952, "id": 96, "name": "Jason"}, {"count": 952, "id": 97, "n* Closing connection 0
ame": "Aquila"}, {"count": 952, "id": 98, "name": "Maile"}, {"count": 952, "id": 99, "name": "Roanna"}, {"count": 952, "id": 100, "name": "Leandra"}]}

#12

What if you do something like below, does that not suffice?

result = session.query(Login).filter(Login.id == id_).one_or_none()
self.response.payload = result

Also, you can try out dictalchemy - below is how Zato uses it internally, this lets you then easily call .asdict() on models to get a model as a dict object, which can be assigned to self.response.payload directly or pre-processed as needed:


#13

Please open a ticket with this traceback along with the full body of the service. Thank you.


#14

I belive the reason of my post was misunderstood, so I’ll rephrase :slight_smile:

When passing a boolean as optional input, you have three possible scenarios:

  1. mybool is not passed, therefore the default value in the schema must prevail when saving the contents into the database through SQLAlchemy.
  2. mybool is passed as false, therefore False must be the value saved, whatever the default value in the schema is.
  3. Idem. when mybool is True.

By adding the default value per attribute feature one can now match the default value in the schema with the default value of the field, which is great when creating a new record.

But when updating a record, if you want to allow partial updates (i.e. all input fields are optional), then you are, again, presented with a similar scenario:

  1. mybool is not passed, therefore the current value in the database must be respected.
  2. mybool is passed, therefore the current value in the database must be overwritten.

So that is why I presented my Update() service, in which I am handling the abovementioned situation by using a series of conditions right after getting the record from the database and before committing the changes. Since it’s the first time doing this, I was requesting advice. :blush:

Finally, regarding your question, assigning result to the payload works fine, and has been working fine since I started testing Zato :ok_hand:


#15

Ah, yes, I understand it now.

I can think of a few things here:

  • What you are doing is fine, this works and will continue to work
  • Again, you can give dictalchemy a try - here is a link to a part of how it is used internally. Observe that with dictalchemy your SQLAlchemy instances have a method called .fromdict and self.request.input happens to be a dict-like object so you can build some interesting features around it.
  • I note that it would be good to be able to specify SIO in terms such as below - both for input and output. Right now one needs to repeat a few bits in both SQLAlchemy and SIO so using models directly in SIO definitions would save time - feel free to open a ticket and it can be done once I get to the SimpleIO refactoring works slated for the next release.
from mymodel import UserModel

class SimpleIO:
  input_required = 'id'
  output_required = UserModel
from mymodel import UserModel

class SimpleIO:
  input_required = UserModel, 'another_attr1', 'another_attr2'
  output_required = 'is_success'

#16

I have opened the ticket on Github. Thanks for your time! :slight_smile:


#17

I see it’s doing the same, but leaving the workload to DictAlchemy. That is a nice, time-saving feature. I’ll give it a try! :slight_smile:

Your proposal would, indeed, be very welcome. If you think of resource based methods, then it makes all the sense in the world to work with such resources as basis. I can also think of quite a bit of complexity on its implementation, for instance when dealing with new/updated records. I’ve openened the ticket on Github. Cheers!


#18

Initially, it would mean only extending SIO to cover definitions based on direct access to SQLAlchemy models - it would not entail REST at this point.

For instance, it would not mean returning any particular HTTP headers, remember that SIO is not limited to REST, yet integrating it closer with REST surely sounds like one of the next steps.

But for now I just wanted to catch this requirement in a GitHub ticket.


#19

From what I’ve seen on the online documentation of dictalchemy and the tests I’ve run, there is no paramter to tell it to skip the empty keys from the input dictionary.

I’ve seen on your example code that you do somehow take the situation into consideration, but by enabling a custom-made skip_input_paramsparameter with the list of required attributes that you want to skip (but you do that manually, as far as I can tell).

Therefore, ideally, to simplify things, I would have a skip_empty_input_keys parameter on SimpleIO to help with partial updates, which would allow the programmer to just pass self.request.input to .fromdict().

Meanwhile, I could write a wrapper that would receive the value of input_optional and the value of self.request.input and do the same. It would still have to have a special consideration for booleans, as far as I know.

What do you think?


#20

There is already parameter called skip_input_keys but it covers only SIO responses. It would not be very difficult to make it work with requests too. Maybe you would want to give it a try and send a PR on GitHub then?

In zato.server.service.reqresp, around line 187-195 you can see how self.input becomes populated with optional parameters.

About the only things required are:

  • Set self.skip_empty_keys just like self.int_parameter_suffixes is set in line 116
  • Wrap that block of code above in line 187-195 in an “if” contingent on the newly added self.skip_empty_keys

But, since this is not backwards-compatible, I will also need to add a flag to server.conf to make it possible to enable it explicitly.