Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full layout metadata, layout named valueList, dateformats #63

Merged
merged 12 commits into from Jan 18, 2024

Conversation

LeJeko
Copy link
Contributor

@LeJeko LeJeko commented Dec 27, 2023

The current version of fmrest limits the data returned by the get_layout function to fieldMetaData, omitting PortalMetaData and valueLists.
=> return response.get('fieldMetaData', None)

As I also needed access to the valueLists for my own development, I had to modify fmrest directly.

  • To ensure backward compatibility, I've added a get_layout_metadata function that returns ALL layout metadata.
  • Based on this, added a get_layout_valueList(name) function to retrieve a named valueList from a layout, formatted for direct use in a form's SELECT.
  • Added optional dateformats parameter to API requests.

fmrest/server.py Outdated
for use in a form SELECT (tuple)

Example:
values = fms.get_layout_valueList()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing name parameter in example: fms.get_layout_valueList('myListValue', 'myOptionalLayoutName')

Copy link
Owner

@davidhamann davidhamann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I left some comments in the code for suggestions/questions/remarks. Let me know what you think.

fmrest/const.py Outdated
@@ -10,6 +10,7 @@
TIMEOUT = int(os.environ.get('fmrest_timeout', 10))

API_VERSIONS = ('v1', 'v2', 'vLatest')
API_DATEFORMATS = ('0', '1', '2')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer API_DATE_FORMATS here to be in line with the snake_case formatting.

Also, can't we use integers here? The API seems to be OK with sending the date format as integer (e.g. in the find request's POST data). In the past some parameters caused issues when sent as integer (for example offset and limit weirdly always needed to be strings to be accepted). But I think they fixed this in the DAPI before introducing the dateformats parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK for snake_case formatting.
I choose str to be in line with API format specified in apidoc:
https://you_host/fmi/data/apidoc/#tag/records/operation/getRecords

QUERY PARAMETERS
dateformats
string
The date format. Use 0 for US, 1 for file locale, or 2 for ISO8601. If not specified, the default value is 0.

fmrest/server.py Outdated
@@ -371,7 +371,8 @@ def get_record(self, record_id: int, portals: Optional[List[Dict]] = None,
scripts: Optional[Dict[str, List]] = None,
layout: Optional[str] = None,
request_layout: Optional[str] = None,
response_layout: Optional[str] = None) -> Record:
response_layout: Optional[str] = None,
dateformats: Optional[str] = None) -> Record:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to call the parameter date_format (singular) to be in line with the other naming, even though it later gets translated to dateformats (plural, one word) for the API. This would then need to be consistent for all the other methods as well.

Also, it would be nice if we could pass keywords instead of the numbers; maybe us, file, iso-8601? Not passing a value or passing the default None would default to whatever the default of the DAPI is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok to use date_format parameter if you prefer. I just use same name to avoid typing error ;-)

If we use keywords, we also need to had logic to convert them.
Is this code a way to to that ?

API_DATE_FORMATS = [('us', '0'), ('file', '1'), ('iso-8601', '2')]

date_format = 'file'

matching_format = next((format[1] for format in API_DATE_FORMATS if format[0] == date_format), None)
if matching_format:
    params['dateformats'] = matching_format

fmrest/server.py Outdated
@@ -413,6 +418,9 @@ def get_record(self, record_id: int, portals: Optional[List[Dict]] = None,

params = build_portal_params(portals, True) if portals else {}

# Date format: 0-US (MM/DD/YYYY, default), 1-file locale, 2-ISO8601 (YYYY-MM-DD)
params['dateformats'] = dateformats if dateformats in API_DATEFORMATS else '0'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the default format could change for the DAPI in a different API version, I wonder if it might be better to not specify any date format when the user doesn't explicitly passes one to this method. Otherwise, we might hardcode the 0 = US value, even if a v3 of the DAPI might switch to a default value of iso-8601.

What do you think?

Preferably, the default for the library would be the ISO format, but I guess that's too late and wouldn't be backward compatible :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think it would make more sense to have the ISO format by default, but Claris has chosen the 0-US format.
Is it a good idea to change the default behavior of FMS?
And it may indeed be a little late to change the default value for those already using the library.

fmrest/server.py Outdated
@@ -797,11 +820,13 @@ def get_scripts(self) -> Dict:

@_with_auto_relogin
def get_layout(self, layout: Optional[str] = None) -> Dict:
"""Fetches layout metadata and returns Dict instance
"""Fetches layout metadata and returns "fieldMetaData" Dict instance
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that it's not optimal that get_layout only returns the fieldMetaData. However, instead of creating a new function that does the same except for its return value, I would suggest we give get_layout a new parameter to control what is returned (and default to the current case to remain backward compatible). For example: metadata=fields in the function signature, and then have options for portals, value_lists, or all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this ?

def get_layout(self, layout: Optional[str] = None, metadata: Optional[str] = None) -> Dict:
        """Fetches layout metadata and returns Dict instance of metadata parameters

        Parameters
        -----------
        layout : str, optional
            Sets the layout name for this request. This takes precedence over
            the value stored in the Server instance's layout attribute
        metadata : str, optional
            Options to get all or specifics metadatas.
            Choices are 'fields', 'portals', 'value_lists' or 'all'.
            Default is 'fields'
        """
        target_layout = layout if layout else self.layout
        path = self._get_api_path('meta.layouts') + f'/{target_layout}'

        response = self._call_filemaker('GET', path)

        if metadata == 'all':
            return response
        elif metadata == 'portals':
            return response.get('portalMetaData', None)
        elif metadata == 'value_lists':
            return response.get('valueLists', None)
        else:
            return response.get('fieldMetaData', None)

fmrest/server.py Outdated

return response

def get_layout_valueList(self, name: str, layout: Optional[str] = None):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest renaming get_layout_valueList to get_value_list_values such that it's clear that we don't fetch meta data for the value list (as we might want to do later on), but only the values itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

fmrest/server.py Outdated

def get_layout_valueList(self, name: str, layout: Optional[str] = None):
"""Retrieves layout metadata and returns a list of "name" values
for use in a form SELECT (tuple)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You write "returns a list of..." but at the end return a tuple. I think returning a list of tuples would actually be a good approach (i.e. [("a", "A"), ("b", "B"), ...]) such that users could still modify the returned list without converting it back list again.

Or was there a specific reason for using a tuple of tuples vs. a list of tuples?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with having a list of tuples as the default output. Therefore, I suggest adding a parameter with the options 'list' (default) and 'tuple' to choose the output format.

In my case, I use it with Django for the CHOICES of a form SELECT, as in a FileMaker Pop-up. For this usage, I specifically need a tuple of tuples.

fmrest/server.py Outdated
name : str
The list name to retreive values
fms : object
fmrest instance to query
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outdated comment for fms?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, because I now use 'self' instead.

fmrest/server.py Outdated
"""
target_layout = layout if layout else self.layout
metadata = self.get_layout_metadata(target_layout)
valueLists = metadata.get('valueLists', None)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use snake case for the variable name, i.e. value_lists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

fmrest/server.py Outdated
for value in evaluation_values:
actual_value = value['value']
display_value = value['displayValue']
options.append((actual_value, display_value))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could speed up the loop where you identify the right value list with an early return or break. Something like this (you don't have to use a list comprehension, only the break really makes the difference):

for vlist in value_lists:
    if vlist['name'] == name:
        values += [(v['value'], v['displayValue']) for v in vlist['values']]
        break

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, well done!

fmrest/server.py Outdated
metadata = self.get_layout_metadata(target_layout)
valueLists = metadata.get('valueLists', None)

# Initializing the list for the Select form
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer not to reference a potential use case (e.g. building the list for an HTML form, or such) in the method since this would be an implementation detail of the user. They might use it to populate a form, or do something completely different with the returned data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, got it

@LeJeko
Copy link
Contributor Author

LeJeko commented Jan 13, 2024

Thanks for your comments. See replies.

@davidhamann
Copy link
Owner

Thanks for your work! Looks all good to me. Just two small things:

1.) Can you put the date keyword logic into its own method, such that we have this logic only at one place? Something like this:

def _date_format_for_keyword(self, keyword: str) -> Optional[str]:
    return next((format[1] for format in API_DATE_FORMATS if format[0] == date_format), None)

2.) I would prefer not to have an output parameter for the get_value_list_values method as it's a specific use-case, would require a union return type (can then be a list or a tuple) and is just one more function call in users' code. Would it be possible for you, to just do tuple(fms.get_value_list_values(...)) in your code vs. in the library?

Thanks again for your work. Happy to merge with these last changes.

@LeJeko
Copy link
Contributor Author

LeJeko commented Jan 16, 2024

I agree with your comments. Code committed.

@davidhamann davidhamann merged commit 62ba449 into davidhamann:master Jan 18, 2024
@davidhamann
Copy link
Owner

Merged, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants