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

feat: add Artifactory Query Language integration #72

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

saper
Copy link

@saper saper commented Nov 25, 2020

This is a rebase of #30

Copy link
Collaborator

@nymous nymous left a comment

Choose a reason for hiding this comment

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

Hello @anancarv and @saper, I am very sorry I took more than a year to review this Ananias 😓
Thank you @saper for your work on the rebase! None of the remarks here are directed towards you, but if you want to work on them more, feel free ^^
I will "request changes" here just to make sure we discuss about it, but I'm overall ok with the PR

Comment on lines +14 to +18
class DomainQueryEnum(str, Enum):
"Artifactory domain objects to be queried"
items = "items"
builds = "builds"
entries = "entries"
Copy link
Collaborator

Choose a reason for hiding this comment

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

If I understand correctly this part of the docs

AQL is constructed as a set of interconnected domains as displayed in the diagram below. You may run queries only on one domain at a time, and this is referred to as the Primary domain of the query.
Currently, the following are supported as primary domains: Item, Build, Entry, Promotion and Release. i.e., your queries may be of the form: items.find(...), builds.find(...), archive.entries.find(...), build.promotions.find(...) or releases.find(...).

Artifactory actually supports more than just the "items, builds or entries" that they announce. And even more, you cannot just use entries.find() (or at least on our instance it returns "Failedtoparsequery").

FYI I tested all the primary domains they advertise in the quoted docs on our instance, and none of them fail (we have no results for releases and build.promotions but that may be because we never used the feature)

class Aql(BaseModel):
"Artifactory Query Language"
domain: DomainQueryEnum = DomainQueryEnum.items
find: Optional[Dict[str, Union[str, List[Dict[str, Any]], Dict[str, str]]]]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if you could make a self-referential type here...
The "find" criterion can be a Dict of:

  • strings
  • dict of strings
  • list of find criteria

But after thinking about it I'm not sure you could extract such a type with forward references, because it's not a Pydantic model but a union of many types...
Maybe something like this could work:

AqlFindCriterion = Dict[str, Union[str, Dict[str, str], List["AqlFindCriterion"]]]

@@ -950,3 +954,53 @@ def delete(self, artifact_path: str) -> None:
artifact_path = artifact_path.lstrip("/")
self._delete(f"{artifact_path}")
logger.debug("Artifact %s successfully deleted", artifact_path)


def create_aql_query(aql_object: Aql):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe this function could become a method in the Aql class, to be used something like this:

aql = Aql(<params>)
aql.to_query()  # items.find().include()...

"Artifactory Query Language support"
_uri = "search/aql"

def query(self, aql_object: Aql) -> List[Dict[str, Union[str, List]]]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

But then I wondered about the API we expose... Should the user create an Aql object, and pass it to the method, or should the method take parameters like find, include... that would individually be typed?
I don't really know the purpose of the Aql object, but maybe you thought about it and have a reason that I don't know ^^

Comment on lines +997 to +999
response_content: List[Dict[str, Union[str, List]]] = response.json()[
"results"
]
Copy link
Collaborator

Choose a reason for hiding this comment

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

You should parse the response here, with parse_from_obj(List[Dict[str, Union[str, List]]], response.json()["results"] (and you won't have to type it because parse_from_obj is cleverly typed 😉)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Wait, actually should we return only the results key? The user might need the other keys, for example to know how many results were returned.



@responses.activate
def test_aql_fail_baq_query():
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wooops, that should read ^^

Suggested change
def test_aql_fail_baq_query():
def test_aql_fail_bad_query():

Comment on lines +961 to +966
aql_query_text = f"{aql_object.domain}.find"

if aql_object.find:
aql_query_text += f"({json.dumps(aql_object.find)})"
else:
aql_query_text += "()"
Copy link
Collaborator

Choose a reason for hiding this comment

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

To keep it consistent with the other parts of the function (and simpler to understand), maybe move the .find to the if block:

Suggested change
aql_query_text = f"{aql_object.domain}.find"
if aql_object.find:
aql_query_text += f"({json.dumps(aql_object.find)})"
else:
aql_query_text += "()"
aql_query_text = f"{aql_object.domain}"
if aql_object.find:
aql_query_text += f".find({json.dumps(aql_object.find)})"
else:
aql_query_text += ".find()"


if aql_object.include:
format_include = (
json.dumps(aql_object.include).replace("[", "").replace("]", "")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure about this, what would happen if there is a [ in one of the included fields? It would probably not happen, I know, but maybe just removing them as the first and last characters would be safer?

Comment on lines +979 to +983
if aql_object.offset:
aql_query_text += f".offset({aql_object.offset})"

if aql_object.limit:
aql_query_text += f".limit({aql_object.limit})"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Be careful with this sort of comparison, if something is not the same as if something is not None! In the first case you will not enter the if if the user sends a 0!

@@ -950,3 +954,53 @@ def delete(self, artifact_path: str) -> None:
artifact_path = artifact_path.lstrip("/")
self._delete(f"{artifact_path}")
logger.debug("Artifact %s successfully deleted", artifact_path)


def create_aql_query(aql_object: Aql):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also I think this method could benefit from being more unit tested, to make sure we generate the correct query whatever the combination of parameters we give. I can write those if you want 😉

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.

4 participants