nailgun.entity_mixins

Defines a set of mixins that provide tools for interacting with entities.

exception nailgun.entity_mixins.BadValueError

Indicates that an inappropriate value was assigned to an entity.

nailgun.entity_mixins.CREATE_MISSING = False

Used by nailgun.entity_mixins.EntityCreateMixin.create_raw().

This is the default value for the create_missing argument to nailgun.entity_mixins.EntityCreateMixin.create_raw(). Keep in mind that this variable also affects methods which call create_raw, such as nailgun.entity_mixins.EntityCreateMixin.create_json().

nailgun.entity_mixins.DEFAULT_SERVER_CONFIG = None

A nailgun.config.ServerConfig object.

Used by nailgun.entity_mixins.Entity.

class nailgun.entity_mixins.Entity(server_config=None, **kwargs)

A representation of a logically related set of API paths.

This class is rather useless as is, and it is intended to be subclassed. Subclasses can specify two useful types of information:

  • fields
  • metadata

Fields and metadata are represented by the _fields and _meta instance attributes, respectively. Here is an example of how to define and instantiate an entity:

>>> class User(Entity):
...     def __init__(self, server_config=None, **kwargs):
...         self._fields = {
...             'name': StringField(),
...             'supervisor': OneToOneField('User'),
...             'subordinate': OneToManyField('User'),
...         }
...         self._meta = {'api_path': 'api/users'}
...         return super(User, self).__init__(server_config, **kwargs)
...
>>> user = User(
...     name='Alice',
...     supervisor=User(id=1),
...     subordinate=[User(id=3), User(id=4)],
... )
>>> user.name == 'Alice'
True
>>> user.supervisor.id = 1
True

The canonical procedure for initializing foreign key fields, shown above, is powerful but verbose. It is tiresome to write statements such as [User(id=3), User(id=4)]. As a convenience, entity IDs may be given:

>>> User(name='Alice', supervisor=1, subordinate=[3, 4])
>>> user.name == 'Alice'
True
>>> user.supervisor.id = 1
True

An entity object is useless if you are unable to use it to communicate with a server. The solution is to provide a nailgun.config.ServerConfig when instantiating a new entity.

  1. If the server_config argument is specified, then that is used.
  2. Otherwise, if nailgun.entity_mixins.DEFAULT_SERVER_CONFIG is set, then that is used.
  3. Otherwise, call nailgun.config.ServerConfig.get().

An entity’s server configuration is stored as a private instance variaable and is used by mixin methods, such as nailgun.entity_mixins.Entity.path(). For more information on server configuration objects, see nailgun.config.BaseServerConfig.

Raises:
compare(other, filter_fcn=None)

Returns True if properties can be compared in terms of eq. Entity’s Fields can be filtered accordingly to ‘filter_fcn’. This callable receives field’s name as first parameter and field itself as second parameter. It must return True if field’s value should be included on comparison and False otherwise. If not provided field’s marked as unique will not be compared by default. ‘id’ and ‘name’ are examples of unique fields commonly ignored. Check Entities fields for fields marked with ‘unique=True’

Parameters:
  • other – entity to compare
  • filter_fcn – callable
Returns:

boolean

get_fields()

Return a copy of the fields on the current object.

Returns:A dict mapping field names to :class`nailgun.entity_fields.Field` objects.
get_values()

Return a copy of field values on the current object.

This method is almost identical to vars(self).copy(). However, only instance attributes that correspond to a field are included in the returned dict.

Returns:A dict mapping field names to user-provided values.
path(which=None)

Return the path to the current entity.

Return the path to base entities of this entity’s type if:

  • which is 'base', or
  • which is None and instance attribute id is unset.

Return the path to this exact entity if instance attribute id is set and:

  • which is 'self', or
  • which is None.

Raise NoSuchPathError otherwise.

Child classes may choose to extend this method, especially if a child entity offers more than the two URLs supported by default. If extended, then the extending class should check for custom parameters before calling super:

def path(self, which):
    if which == 'custom':
        return urljoin(…)
    super(ChildEntity, self).__init__(which)

This will allow the extending method to accept a custom parameter without accidentally raising a NoSuchPathError.

Parameters:which – A string. Optional. Valid arguments are ‘self’ and ‘base’.
Returns:A string. A fully qualified URL.
Raises:nailgun.entity_mixins.NoSuchPathError – If no path can be built.
to_json()

Create a JSON encoded string with Entity properties. Ex:

>>> from nailgun import entities, config
>>> kwargs = {
...         'id': 1,
...         'name': 'Nailgun Org',
...     }
>>> org = entities.Organization(config.ServerConfig('foo'), \*\*kwargs)
>>> org.to_json()
'{"id": 1, "name": "Nailgun Org"}'
Returns:str
to_json_dict(filter_fcn=None)

Create a dict with Entity properties for json encoding. It can be overridden by subclasses for each standard serialization doesn’t work. By default it call _to_json_dict on OneToOne fields and build a list calling the same method on each OneToMany object’s fields.

Fields can be filtered accordingly to ‘filter_fcn’. This callable receives field’s name as first parameter and fields itself as second parameter. It must return True if field’s value should be included on dict and False otherwise. If not provided field will not be filtered.

Returns:dct
class nailgun.entity_mixins.EntityCreateMixin

This mixin provides the ability to create an entity.

The methods provided by this class work together. The call tree looks like this:

create
└── create_json
    └── create_raw
        ├── create_missing
        └── create_payload

In short, here is what the methods do:

create_missing()
Populate required fields with random values. Required fields that already have a value are not populated. This method is not called by default.
create_payload()
Assemble a payload of data that can be encoded and sent to the server.
create_raw()
Make an HTTP POST request to the server, including the payload.
create_json()
Check the server’s response for errors and decode the response.
create()
Create a nailgun.entity_mixins.Entity object representing the created entity and populate its fields with data returned from the server.

See the individual methods for more detailed information.

create(create_missing=None)

Create an entity.

Call create_json(), use the response to populate a new object of type type(self) and return that object.

This method requires that a method named “read” be available on the current object. A method named “read” will be available if EntityReadMixin is present in the inheritance tree, and using the method provided by that mixin is the recommended technique for making a “read” method available.

This method makes use of EntityReadMixin.read() for two reasons. First, calling that method is simply convenient. Second, the server frequently returns weirdly structured, inconsistently named or straight-up broken responses, and quite a bit of effort has gone in to decoding server responses so EntityReadMixin.read() can function correctly. Calling read allows this method to re-use the decoding work that has been done for that method.

Returns:An instance of type type(self).
Return type:nailgun.entity_mixins.Entity
Raises:AttributeError if a method named “read” is not available on the current object.
create_json(create_missing=None)

Create an entity.

Call create_raw(). Check the response status code, decode JSON and return the decoded JSON as a dict.

Returns:A dict. The server’s response, with all JSON decoded.
Raises:requests.exceptions.HTTPError if the response has an HTTP 4XX or 5XX status code.
Raises:ValueError If the response JSON can not be decoded.
create_missing()

Automagically populate all required instance attributes.

Iterate through the set of all required class nailgun.entity_fields.Field defined on type(self) and create a corresponding instance attribute if none exists. Subclasses should override this method if there is some relationship between two required fields.

Returns:Nothing. This method relies on side-effects.
create_payload()

Create a payload of values that can be sent to the server.

See _payload().

create_raw(create_missing=None)

Create an entity.

Possibly call create_missing(). Then make an HTTP POST call to self.path('base'). The request payload consists of whatever is returned by create_payload(). Return the response.

Parameters:create_missing – Should create_missing() be called? In other words, should values be generated for required, empty fields? Defaults to nailgun.entity_mixins.CREATE_MISSING.
Returns:A requests.response object.
class nailgun.entity_mixins.EntityDeleteMixin

This mixin provides the ability to delete an entity.

The methods provided by this class work together. The call tree looks like this:

delete → delete_raw

In short, here is what the methods do:

delete_raw()
Make an HTTP DELETE request to the server.
delete()
Check the server’s response for errors and decode the response.
delete(synchronous=True)

Delete the current entity.

Call delete_raw() and check for an HTTP 4XX or 5XX response. Return either the JSON-decoded response or information about a completed foreman task.

Parameters:synchronous – A boolean. What should happen if the server returns an HTTP 202 (accepted) status code? Wait for the task to complete if True. Immediately return a response otherwise.
Returns:A dict. Either the JSON-decoded response or information about a foreman task.
Raises:requests.exceptions.HTTPError if the response has an HTTP 4XX or 5XX status code.
Raises:ValueError If an HTTP 202 response is received and the response JSON can not be decoded.
Raises:nailgun.entity_mixins.TaskTimedOutError – If an HTTP 202 response is received, synchronous is True and the task times out.
delete_raw()

Delete the current entity.

Make an HTTP DELETE call to self.path('base'). Return the response.

Returns:A requests.response object.
class nailgun.entity_mixins.EntityReadMixin

This mixin provides the ability to read an entity.

The methods provided by this class work together. The call tree looks like this:

read → read_json → read_raw

In short, here is what the methods do:

read_raw()
Make an HTTP GET request to the server.
read_json()
Check the server’s response for errors and decode the response.
read()
Create a nailgun.entity_mixins.Entity object representing the created entity and populate its fields with data returned from the server.

See the individual methods for more detailed information.

read(entity=None, attrs=None, ignore=None, params=None)

Get information about the current entity.

  1. Create a new entity of type type(self).
  2. Call read_json() and capture the response.
  3. Populate the entity with the response.
  4. Return the entity.

Step one is skipped if the entity argument is specified. Step two is skipped if the attrs argument is specified. Step three is modified by the ignore argument.

All of an entity’s one-to-one and one-to-many relationships are populated with objects of the correct type. For example, if SomeEntity.other_entity is a one-to-one relationship, this should return True:

isinstance(
    SomeEntity(id=N).read().other_entity,
    nailgun.entity_mixins.Entity
)

Additionally, both of these commands should succeed:

SomeEntity(id=N).read().other_entity.id
SomeEntity(id=N).read().other_entity.read().other_attr

In the example above, other_entity.id is the only attribute with a meaningful value. Calling other_entity.read populates the remaining entity attributes.

Parameters:
  • entity (nailgun.entity_mixins.Entity) – The object to be populated and returned. An object of type type(self) by default.
  • attrs – A dict. Data used to populate the object’s attributes. The response from nailgun.entity_mixins.EntityReadMixin.read_json() by default.
  • ignore – A set of attributes which should not be read from the server. This is mainly useful for attributes like a password which are not returned.
Returns:

An instance of type type(self).

Return type:

nailgun.entity_mixins.Entity

read_json(params=None)

Get information about the current entity.

Call read_raw(). Check the response status code, decode JSON and return the decoded JSON as a dict.

Returns:A dict. The server’s response, with all JSON decoded.
Raises:requests.exceptions.HTTPError if the response has an HTTP 4XX or 5XX status code.
Raises:ValueError If the response JSON can not be decoded.
read_raw(params=None)

Get information about the current entity.

Make an HTTP GET call to self.path('self'). Return the response.

Returns:A requests.response object.
class nailgun.entity_mixins.EntitySearchMixin

This mixin provides the ability to search for entities.

The methods provided by this class work together. The call tree looks like this:

search
├── search_json
│   └── search_raw
│       └── search_payload
├── search_normalize
└── search_filter

In short, here is what the methods do:

search_payload()
Assemble a search query that can be encoded and sent to the server.
search_raw()
Make an HTTP GET request to the server, including the payload.
search_json()
Check the server’s response for errors and decode the response.
search_normalize()
Normalize search results so they can be used to create new entities.
search()
Create one or more nailgun.entity_mixins.Entity objects representing the found entities and populate their fields.
search_filter()
Read all entities and locally filter them.

See the individual methods for more detailed information.

search(fields=None, query=None, filters=None)

Search for entities.

At its simplest, this method searches for all entities of a given kind. For example, to ask for all nailgun.entities.LifecycleEnvironment entities:

LifecycleEnvironment().search()

Values on an entity are used to generate a search query, and the fields argument can be used to specify which fields should be used when generating a search query:

lc_env = LifecycleEnvironment(name='foo', organization=1)
results = lc_env.search()  # Search by name and organization.
results = lc_env.search({'name', 'organization'})  # Same.
results = lc_env.search({'name'})  # Search by name.
results = lc_env.search({'organization'})  # Search by organization
results = lc_env.search(set())  # Search for all lifecycle envs.
results = lc_env.search({'library'})  # Error!

In some cases, the simple search queries that can be generated by NailGun are not sufficient. In this case, you can pass in a raw search query instead. For example, to search for all lifecycle environments with a name of ‘foo’:

LifecycleEnvironment().search(query={'search': 'name="foo"'})

The example above is rather pointless: it is easier and more concise to use a generated query. But — and this is a very important “but” — the manual search query is melded in to the generated query. This can be used to great effect:

LifecycleEnvironment(name='foo').search(query={'per_page': 50})

For examples of what the final search queries look like, see search_payload(). (That method also accepts the fields and query arguments.)

In some cases, the server’s search facilities may be insufficient, or it may be inordinately difficult to craft a search query. In this case, you can filter search results locally. For example, to ask the server for a list of all lifecycle environments and then locally search through the results for the lifecycle environment named “foo”:

LifecycleEnvironment().search(filters={'name': 'foo'})

Be warned that filtering locally can be very slow. NailGun must read() every single entity returned by the server before filtering results. This is because the values used in the filtering process may not have been returned by the server in the initial response to the search.

The fact that all entities are read when filters is specified can be used to great effect. For example, this search returns a fully populated list of every single lifecycle environment:

LifecycleEnvironment().search(filters={})
Parameters:
  • fields – A set naming which fields should be used when generating a search query. If None, all values on the entity are used. If an empty set, no values are used.
  • query – A dict containing a raw search query. This is melded in to the generated search query like so: {generated: query}.update({manual: query}).
  • filters – A dict. Used to filter search results locally.
Returns:

A list of entities, all of type type(self).

static search_filter(entities, filters)

Read all entities and locally filter them.

This method can be used like so:

entities = EntitySearchMixin(entities, {'name': 'foo'})

In this example, only entities where entity.name == 'foo' holds true are returned. An arbitrary number of field names and values may be provided as filters.

Note

This method calls EntityReadMixin.read(). As a result, this method only works when called on a class that also inherits from EntityReadMixin.

Parameters:
  • entities – A list of Entity objects. All list items should be of the same type.
  • filters – A dict in the form {field_name: field_value, …}.
Raises:

nailgun.entity_mixins.NoSuchFieldError – If any of the fields named in filters do not exist on the entities being filtered.

Raises:

NotImplementedError If any of the fields named in filters are a nailgun.entity_fields.OneToOneField or nailgun.entity_fields.OneToManyField.

search_json(fields=None, query=None)

Search for entities.

Call search_raw(). Check the response status code, decode JSON and return the decoded JSON as a dict.

Warning

Subclasses that override this method should not alter the fields or query arguments. (However, subclasses that override this method may still alter the server’s response.) See search_normalize() for details.

Parameters:
Returns:

A dict. The server’s response, with all JSON decoded.

Raises:

requests.exceptions.HTTPError if the response has an HTTP 4XX or 5XX status code.

Raises:

ValueError If the response JSON can not be decoded.

search_normalize(results)

Normalize search results so they can be used to create new entities.

See search() for an example of how to use this method. Here’s a simplified example:

results = self.search_json()
results = self.search_normalize(results)
entity = SomeEntity(some_cfg, **results[0])

At this time, it is possible to parse all search results without knowing what search query was sent to the server. However, it is possible that certain responses can only be parsed if the search query is known. If that is the case, this method will be given a new payload argument, where payload is the query sent to the server.

As a precaution, the following is highly recommended:

  • search() may alter fields and query at will.
  • search_payload() may alter fields and query in an idempotent manner.
  • No other method should alter fields or query.
Parameters:results – A list of dicts, where each dict is a set of attributes for one entity. The contents of these dicts are as is returned from the server.
Returns:A list of dicts, where each dict is a set of attributes for one entity. The contents of these dicts have been normalized and can be used to instantiate entities.
search_payload(fields=None, query=None)

Create a search query.

Do the following:

  1. Generate a search query. By default, all values returned by nailgun.entity_mixins.Entity.get_values() are used. If fields is specified, only the named values are used.
  2. Merge query in to the generated search query.
  3. Return the result.

The rules for generating a search query can be illustrated by example. Let’s say that we have an entity with an nailgun.entity_fields.IntegerField, a nailgun.entity_fields.OneToOneField and a nailgun.entity_fields.OneToManyField:

>>> some_entity = SomeEntity(id=1, one=2, many=[3, 4])
>>> fields = some_entity.get_fields()
>>> isinstance(fields['id'], IntegerField)
True
>>> isinstance(fields['one'], OneToOneField)
True
>>> isinstance(fields['many'], OneToManyField)
True

This method appends “_id” and “_ids” on to the names of each OneToOneField and OneToManyField, respectively:

>>> some_entity.search_payload()
{'id': 1, 'one_id': 2, 'many_ids': [3, 4]}

By default, all fields are used. But you can specify a set of field names to use:

>>> some_entity.search_payload({'id'})
{'id': 1}
>>> some_entity.search_payload({'one'})
{'one_id': 2}
>>> some_entity.search_payload({'id', 'one'})
{'id': 1, 'one_id': 2}

If a query is specified, it is merged in to the generated query:

>>> some_entity.search_payload(query={'id': 5})
{'id': 5, 'one_id': 2, 'many_ids': [3, 4]}
>>> some_entity.search_payload(query={'per_page': 1000})
{'id': 1, 'one_id': 2, 'many_ids': [3, 4], 'per_page': 1000}

Warning

This method currently generates an extremely naive search query that will be wrong in many cases. In addition, Satellite currently accepts invalid search queries without complaint. Make sure to check the API documentation for your version of Satellite against what this method produces.

Parameters:
Returns:

A dict that can be encoded as JSON and used in a search.

search_raw(fields=None, query=None)

Search for entities.

Make an HTTP GET call to self.path('base'). Return the response.

Warning

Subclasses that override this method should not alter the fields or query arguments. (However, subclasses that override this method may still alter the server’s response.) See search_normalize() for details.

Parameters:
Returns:

A requests.response object.

class nailgun.entity_mixins.EntityUpdateMixin

This mixin provides the ability to update an entity.

The methods provided by this class work together. The call tree looks like this:

update → update_json → update_raw → update_payload

In short, here is what the methods do:

update_payload()
Assemble a payload of data that can be encoded and sent to the server.
update_raw()
Make an HTTP PUT request to the server, including the payload.
update_json()
Check the server’s response for errors and decode the response.
update()
Create a nailgun.entity_mixins.Entity object representing the created entity and populate its fields.

See the individual methods for more detailed information.

update(fields=None)

Update the current entity.

Call update_json(), use the response to populate a new object of type type(self) and return that object.

This method requires that nailgun.entity_mixins.EntityReadMixin.read() or some other identical method be available on the current object. A more thorough explanation is available at nailgun.entity_mixins.EntityCreateMixin.create().

Parameters:fields – An iterable of field names. Only the fields named in this iterable will be updated. No fields are updated if an empty iterable is passed in. All fields are updated if None is passed in.
Raises:KeyError if asked to update a field but no value is available for that field on the current entity.
update_json(fields=None)

Update the current entity.

Call update_raw(). Check the response status code, decode JSON and return the decoded JSON as a dict.

Parameters:fields – See update().
Returns:A dict consisting of the decoded JSON in the server’s response.
Raises:requests.exceptions.HTTPError if the response has an HTTP 4XX or 5XX status code.
Raises:ValueError If the response JSON can not be decoded.
update_payload(fields=None)

Create a payload of values that can be sent to the server.

By default, this method behaves just like _payload(). However, one can also specify a certain set of fields that should be returned. For more information, see update().

update_raw(fields=None)

Update the current entity.

Make an HTTP PUT call to self.path('base'). The request payload consists of whatever is returned by update_payload(). Return the response.

Parameters:fields – See update().
Returns:A requests.response object.
exception nailgun.entity_mixins.MissingValueError

Indicates that no value can be found for a field.

exception nailgun.entity_mixins.NoSuchFieldError

Indicates that the referenced field does not exist.

exception nailgun.entity_mixins.NoSuchPathError

Indicates that the requested path cannot be constructed.

nailgun.entity_mixins.TASK_POLL_RATE = 5

Default for poll_rate argument to nailgun.entity_mixins._poll_task().

nailgun.entity_mixins.TASK_TIMEOUT = 300

Default for timeout argument to nailgun.entity_mixins._poll_task().

exception nailgun.entity_mixins.TaskFailedError

Indicates that a task finished with a result other than “success”.

exception nailgun.entity_mixins.TaskTimedOutError

Indicates that a task did not finish before the timout limit.

nailgun.entity_mixins._get_entity_id(field_name, attrs)

Find the ID for a one to one relationship.

The server may return JSON data in the following forms for a nailgun.entity_fields.OneToOneField:

'user': None
'user': {'name': 'Alice Hayes', 'login': 'ahayes', 'id': 1}
'user_id': 1
'user_id': None

Search attrs for a one to one field_name and return its ID.

Parameters:
  • field_name – A string. The name of a field.
  • attrs – A dict. A JSON payload as returned from a server.
Returns:

Either an entity ID or None.

nailgun.entity_mixins._get_entity_ids(field_name, attrs)

Find the IDs for a one to many relationship.

The server may return JSON data in the following forms for a nailgun.entity_fields.OneToManyField:

'user': [{'id': 1, …}, {'id': 42, …}]
'users': [{'id': 1, …}, {'id': 42, …}]
'user_ids': [1, 42]

Search attrs for a one to many field_name and return its ID.

Parameters:
  • field_name – A string. The name of a field.
  • attrs – A dict. A JSON payload as returned from a server.
Returns:

An iterable of entity IDs.

nailgun.entity_mixins._get_server_config()

Search for a nailgun.config.ServerConfig.

Returns:nailgun.entity_mixins.DEFAULT_SERVER_CONFIG if it is not None, or whatever is returned by nailgun.config.ServerConfig.get() otherwise.
Return type:nailgun.config.ServerConfig
nailgun.entity_mixins._make_entities_from_ids(entity_cls, entity_objs_and_ids, server_config)

Given an iterable of entities and/or IDs, return a list of entities.

Parameters:
  • entity_cls – An Entity subclass.
  • entity_obj_or_id – An iterable of nailgun.entity_mixins.Entity objects and/or entity IDs. All of the entities in this iterable should be of type entity_cls.
Returns:

A list of entity_cls objects.

nailgun.entity_mixins._make_entity_from_id(entity_cls, entity_obj_or_id, server_config)

Given an entity object or an ID, return an entity object.

If the value passed in is an object that is a subclass of Entity, return that value. Otherwise, create an object of the type that field references, give that object an ID of field_value, and return that object.

Parameters:
Returns:

An entity_cls object.

Return type:

nailgun.entity_mixins.Entity

nailgun.entity_mixins._payload(fields, values)

Implement the *_payload methods.

It’s frequently useful to create a dict of values that can be encoded to JSON and sent to the server. Unfortunately, there are mismatches between the field names used by NailGun and the field names the server expects. This method provides a default translation that works in many cases. For example:

>>> from nailgun.entities import Product
>>> product = Product(name='foo', organization=1)
>>> set(product.get_fields())
{
    'description',
    'gpg_key',
    'id',
    'label',
    'name',
    'organization',
    'sync_plan',
}
>>> set(product.get_values())
{'name', 'organization'}
>>> product.create_payload()
{'organization_id': 1, 'name': 'foo'}
Parameters:
Returns:

A dict mapping field names to field values.

nailgun.entity_mixins._poll_task(task_id, server_config, poll_rate=None, timeout=None)

Implement nailgun.entities.ForemanTask.poll().

See nailgun.entities.ForemanTask.poll() for a full description of how this method acts. Other methods may also call this method, such as nailgun.entity_mixins.EntityDeleteMixin.delete().

Certain mixins benefit from being able to poll the server after performing an operation. However, this module cannot use nailgun.entities.ForemanTask.poll(), as that would be a circular import. Placing the implementation of nailgun.entities.ForemanTask.poll() here allows both that method and the mixins in this module to use the same logic.

nailgun.entity_mixins.to_json_serializable(obj)

Transforms obj into a json serializable object.

Parameters:obj – entity or any json serializable object
Returns:serializable object