1# encoding: utf-8 2# 3# spyne - Copyright (C) Spyne contributors. 4# 5# This library is free software; you can redistribute it and/or 6# modify it under the terms of the GNU Lesser General Public 7# License as published by the Free Software Foundation; either 8# version 2.1 of the License, or (at your option) any later version. 9# 10# This library is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13# Lesser General Public License for more details. 14# 15# You should have received a copy of the GNU Lesser General Public 16# License along with this library; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 18# 19 20"""Useful stuff to integrate Spyne with Django. 21 22* Django model <-> spyne type mapping 23* Service for common exception handling 24 25""" 26 27from __future__ import absolute_import 28 29import logging 30logger = logging.getLogger(__name__) 31 32import re 33 34from itertools import chain 35 36from django.core.exceptions import (ImproperlyConfigured, ObjectDoesNotExist, 37 ValidationError as DjValidationError) 38from django.core.validators import (slug_re, 39 MinLengthValidator, MaxLengthValidator) 40try: 41 from django.core.validators import comma_separated_int_list_re 42except ImportError: 43 comma_separated_int_list_re = re.compile(r'^[\d,]+$') 44 45from spyne.error import (ResourceNotFoundError, ValidationError as 46 BaseValidationError, Fault) 47from spyne.model import primitive 48from spyne.model.complex import ComplexModelMeta, ComplexModelBase 49from spyne.service import Service 50from spyne.util.cdict import cdict 51from spyne.util.odict import odict 52from spyne.util.six import add_metaclass 53 54 55# regex is based on http://www.w3.org/TR/xforms20/#xforms:email 56email_re = re.compile( 57 r"[A-Za-z0-9!#-'\*\+\-/=\?\^_`\{-~]+" 58 r"(\.[A-Za-z0-9!#-'\*\+\-/=\?\^_`\{-~]+)*@" 59 # domain part is either a single symbol 60 r"(([a-zA-Z0-9]|" 61 # or have at least two symbols 62 # hyphen can't be at the beginning or end of domain part 63 # domain should contain at least 2 parts, the last one is TLD 64 r"([a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])+)\.)+" 65 # TLD should contain only letters, at least 2 66 r"[A-Za-z]{2,}", re.IGNORECASE) 67 68 69def _handle_minlength(validator, params): 70 new_min = validator.limit_value 71 old_min = params.setdefault('min_len', new_min) 72 params['min_len'] = max(old_min, new_min) 73 74 75def _handle_maxlength(validator, params): 76 new_max = validator.limit_value 77 old_max = params.setdefault('max_len', new_max) 78 params['max_len'] = min(old_max, new_max) 79 80 81class BaseDjangoFieldMapper(object): 82 83 """Abstrace base class for field mappers.""" 84 85 _VALIDATOR_HANDLERS = cdict({ 86 MinLengthValidator: _handle_minlength, 87 MaxLengthValidator: _handle_maxlength, 88 }) 89 90 @staticmethod 91 def is_field_nullable(field, **kwargs): 92 """Return True if django field is nullable.""" 93 return field.null 94 95 @staticmethod 96 def is_field_blank(field, **kwargs): 97 """Return True if django field is blank.""" 98 return field.blank 99 100 def map(self, field, **kwargs): 101 """Map field to spyne model. 102 103 :param field: Django Field instance 104 :param kwargs: Extra params to configure spyne model 105 :returns: tuple (field attribute name, mapped spyne model) 106 107 """ 108 params = kwargs.copy() 109 110 self._process_validators(field.validators, params) 111 112 nullable = self.is_field_nullable(field, **kwargs) 113 blank = self.is_field_blank(field, **kwargs) 114 required = not (field.has_default() or blank or field.primary_key) 115 116 if field.has_default(): 117 params['default'] = field.get_default() 118 119 spyne_model = self.get_spyne_model(field, **kwargs) 120 customized_model = spyne_model(nullable=nullable, 121 min_occurs=int(required), **params) 122 123 return (field.attname, customized_model) 124 125 def get_spyne_model(self, field, **kwargs): 126 """Return spyne model for given Django field.""" 127 raise NotImplementedError 128 129 def _process_validators(self, validators, params): 130 for v in validators: 131 handler = self._VALIDATOR_HANDLERS.get(type(v)) 132 if handler: 133 handler(v, params) 134 135 136class DjangoFieldMapper(BaseDjangoFieldMapper): 137 138 """Basic mapper for django fields.""" 139 140 def __init__(self, spyne_model): 141 """Django field mapper constructor.""" 142 self.spyne_model = spyne_model 143 144 def get_spyne_model(self, field, **kwargs): 145 """Return configured spyne model.""" 146 return self.spyne_model 147 148 149class DecimalMapper(DjangoFieldMapper): 150 151 """Mapper for DecimalField.""" 152 153 def map(self, field, **kwargs): 154 """Map DecimalField to spyne model. 155 156 :returns: tuple (field attribute name, mapped spyne model) 157 158 """ 159 params = kwargs.copy() 160 params.update({ 161 'total_digits': field.max_digits, 162 'fraction_digits': field.decimal_places, 163 }) 164 return super(DecimalMapper, self).map(field, **params) 165 166 167class RelationMapper(BaseDjangoFieldMapper): 168 169 """Mapper for relation fields (ForeignKey, OneToOneField).""" 170 171 def __init__(self, django_model_mapper): 172 """Constructor for relation field mapper.""" 173 self.django_model_mapper = django_model_mapper 174 175 @staticmethod 176 def is_field_blank(field, **kwargs): 177 """Return True if `optional_relations` is set. 178 179 Otherwise use basic behaviour. 180 181 """ 182 optional_relations = kwargs.get('optional_relations', False) 183 return (optional_relations or 184 BaseDjangoFieldMapper.is_field_blank(field, **kwargs)) 185 186 def get_spyne_model(self, field, **kwargs): 187 """Return spyne model configured by related field.""" 188 related_field = field.rel.get_related_field() if hasattr(field, 'rel') else field.remote_field.get_related_field() 189 field_type = related_field.__class__.__name__ 190 field_mapper = self.django_model_mapper.get_field_mapper(field_type) 191 192 _, related_spyne_model = field_mapper.map(related_field, **kwargs) 193 return related_spyne_model 194 195 196class DjangoModelMapper(object): 197 198 r"""Mapper from django models to spyne complex models. 199 200 You can extend it registering new field types: :: 201 202 class NullBooleanMapper(DjangoFieldMapper): 203 204 def map(self, field, **kwargs): 205 params = kwargs.copy() 206 # your mapping logic goes here 207 return super(NullBooleanMapper, self).map(field, **params) 208 209 default_model_mapper.register_field_mapper('NullBooleanField', \ 210 NullBooleanMapper(primitive.Boolean)) 211 212 213 You may subclass it if you want different mapping logic for different 214 Django models. 215 216 """ 217 218 field_mapper_class = DjangoFieldMapper 219 220 class UnknownFieldMapperException(Exception): 221 222 """Raises when there is no field mapper for given django_type.""" 223 224 def __init__(self, django_spyne_models=()): 225 """Register field mappers in internal registry.""" 226 self._registry = {} 227 228 for django_type, spyne_model in django_spyne_models: 229 self.register(django_type, spyne_model) 230 231 def get_field_mapper(self, django_type): 232 """Get mapper registered for given django_type. 233 234 :param django_type: Django internal field type 235 :returns: registered mapper 236 :raises: :exc:`UnknownFieldMapperException` 237 238 """ 239 try: 240 return self._registry[django_type] 241 except KeyError: 242 raise self.UnknownFieldMapperException( 243 'No mapper for field type {0}'.format(django_type)) 244 245 def register(self, django_type, spyne_model): 246 """Register default field mapper for django_type and spyne_model. 247 248 :param django_type: Django internal field type 249 :param spyne_model: Spyne model, usually primitive 250 251 """ 252 field_mapper = self.field_mapper_class(spyne_model) 253 self.register_field_mapper(django_type, field_mapper) 254 255 def register_field_mapper(self, django_type, field_mapper): 256 """Register field mapper for django_type. 257 258 :param django_type: Django internal field type 259 :param field_mapper: :class:`DjangoFieldMapper` instance 260 261 """ 262 self._registry[django_type] = field_mapper 263 264 @staticmethod 265 def get_all_field_names(meta): 266 if hasattr(meta, 'get_all_field_names'): 267 return meta.get_all_field_names() 268 269 return list(set(chain.from_iterable( 270 (field.name, field.attname) if hasattr(field, 'attname') else ( 271 field.name,) 272 for field in meta.get_fields() 273 # For complete backwards compatibility, you may want to exclude 274 # GenericForeignKey from the results. 275 if not (field.many_to_one and field.related_model is None) 276 ))) 277 278 @staticmethod 279 def _get_fields(django_model, exclude=None): 280 field_names = set(exclude) if exclude is not None else set() 281 meta = django_model._meta # pylint: disable=W0212 282 unknown_fields_names = \ 283 field_names.difference(DjangoModelMapper.get_all_field_names(meta)) 284 285 if unknown_fields_names: 286 raise ImproperlyConfigured( 287 'Unknown field names: {0}' 288 .format(', '.join(unknown_fields_names))) 289 290 return [field for field in meta.fields if field.name not in 291 field_names] 292 293 def map(self, django_model, exclude=None, **kwargs): 294 """Prepare dict of model fields mapped to spyne models. 295 296 :param django_model: Django model class. 297 :param exclude: list of fields excluded from mapping. 298 :param kwargs: extra kwargs are passed to all field mappers 299 300 :returns: dict mapping attribute names to spyne models 301 :raises: :exc:`UnknownFieldMapperException` 302 303 """ 304 field_map = odict() 305 306 for field in self._get_fields(django_model, exclude): 307 field_type = field.__class__.__name__ 308 309 try: 310 field_mapper = self._registry[field_type] 311 except KeyError: 312 # mapper for this field is not registered 313 if not (field.has_default() or field.null): 314 # field is required 315 raise self.UnknownFieldMapperException( 316 'No mapper for field type {0}'.format(field_type)) 317 else: 318 # skip this field 319 logger.info('Field {0} is skipped from mapping.') 320 continue 321 322 attr_name, spyne_model = field_mapper.map(field, **kwargs) 323 field_map[attr_name] = spyne_model 324 325 return field_map 326 327 328def strip_regex_metachars(pattern): 329 """Strip ^ and $ from pattern begining and end. 330 331 According to http://www.w3.org/TR/xmlschema-0/#regexAppendix XMLSchema 332 expression language does not contain the metacharacters ^ and $. 333 334 :returns: stripped pattern string 335 336 """ 337 start = 0 338 till = len(pattern) 339 340 if pattern.startswith('^'): 341 start = 1 342 343 if pattern.endswith('$'): 344 till -= 1 345 346 return pattern[start:till] 347 348 349# django's own slug_re.pattern is invalid according to xml schema -- it doesn't 350# like the location of the dash character. using the equivalent pattern accepted 351# by xml schema here. 352SLUG_RE_PATTERN = '[a-zA-Z0-9_-]+' 353 354 355DEFAULT_FIELD_MAP = ( 356 ('AutoField', primitive.Integer32), 357 ('CharField', primitive.NormalizedString), 358 ('SlugField', primitive.Unicode( 359 type_name='Slug', pattern=strip_regex_metachars(SLUG_RE_PATTERN))), 360 ('TextField', primitive.Unicode), 361 ('EmailField', primitive.Unicode( 362 type_name='Email', pattern=strip_regex_metachars(email_re.pattern))), 363 ('CommaSeparatedIntegerField', primitive.Unicode( 364 type_name='CommaSeparatedField', 365 pattern=strip_regex_metachars(comma_separated_int_list_re.pattern))), 366 ('URLField', primitive.AnyUri), 367 ('FilePathField', primitive.Unicode), 368 369 ('BooleanField', primitive.Boolean), 370 ('NullBooleanField', primitive.Boolean), 371 ('IntegerField', primitive.Integer), 372 ('BigIntegerField', primitive.Integer64), 373 ('PositiveIntegerField', primitive.UnsignedInteger32), 374 ('SmallIntegerField', primitive.Integer16), 375 ('PositiveSmallIntegerField', primitive.UnsignedInteger16), 376 ('FloatField', primitive.Double), 377 378 ('TimeField', primitive.Time), 379 ('DateField', primitive.Date), 380 ('DateTimeField', primitive.DateTime), 381 382 # simple fixed defaults for relation fields 383 ('ForeignKey', primitive.Integer32), 384 ('OneToOneField', primitive.Integer32), 385) 386 387 388def model_mapper_factory(mapper_class, field_map): 389 """Factory for model mappers. 390 391 The factory is useful to create custom field mappers based on default one. 392 393 """ 394 model_mapper = mapper_class(field_map) 395 396 # register relation field mappers that are aware of related field type 397 model_mapper.register_field_mapper( 398 'ForeignKey', RelationMapper(model_mapper)) 399 400 model_mapper.register_field_mapper( 401 'OneToOneField', RelationMapper(model_mapper)) 402 403 model_mapper.register_field_mapper('DecimalField', 404 DecimalMapper(primitive.Decimal)) 405 return model_mapper 406 407 408default_model_mapper = model_mapper_factory(DjangoModelMapper, 409 DEFAULT_FIELD_MAP) 410 411 412class DjangoComplexModelMeta(ComplexModelMeta): 413 414 """Meta class for complex spyne models representing Django models.""" 415 416 def __new__(mcs, name, bases, attrs): # pylint: disable=C0202 417 """Populate new complex type from configured Django model.""" 418 super_new = super(DjangoComplexModelMeta, mcs).__new__ 419 420 abstract = bool(attrs.get('__abstract__', False)) 421 422 if abstract: 423 # skip processing of abstract models 424 return super_new(mcs, name, bases, attrs) 425 426 attributes = attrs.get('Attributes') 427 428 if attributes is None: 429 raise ImproperlyConfigured('You have to define Attributes and ' 430 'specify Attributes.django_model') 431 432 if getattr(attributes, 'django_model', None) is None: 433 raise ImproperlyConfigured('You have to define django_model ' 434 'attribute in Attributes') 435 436 mapper = getattr(attributes, 'django_mapper', default_model_mapper) 437 attributes.django_mapper = mapper 438 exclude = getattr(attributes, 'django_exclude', None) 439 optional_relations = getattr(attributes, 'django_optional_relations', 440 False) 441 spyne_attrs = mapper.map(attributes.django_model, exclude=exclude, 442 optional_relations=optional_relations) 443 spyne_attrs.update(attrs) 444 return super_new(mcs, name, bases, spyne_attrs) 445 446 447@add_metaclass(DjangoComplexModelMeta) 448class DjangoComplexModel(ComplexModelBase): 449 450 """Base class with Django model mapping support. 451 452 Sample usage: :: 453 454 class PersonType(DjangoComplexModel): 455 class Attributes(DjangoComplexModel.Attributes): 456 django_model = Person 457 458 459 Attribute :attr:`django_model` is required for Django model mapping 460 machinery. You can customize your types defining custom type fields: :: 461 462 class PersonType(DjangoComplexModel): 463 gender = primitive.Unicode(pattern='^[FM]$') 464 465 class Attributes(DjangoComplexModel.Attributes): 466 django_model = Person 467 468 469 There is an option to specify custom mapper: :: 470 471 class PersonType(DjangoComplexModel): 472 class Attributes(DjangoComplexModel.Attributes): 473 django_model = Person 474 django_mapper = my_custom_mapper 475 476 You can also exclude some fields from mapping: :: 477 478 class PersonType(DjangoComplexModel): 479 class Attributes(DjangoComplexModel.Attributes): 480 django_model = Person 481 django_exclude = ['phone'] 482 483 You may set `django_optional_relations`` attribute flag to indicate 484 that relation fields (ForeignKey, OneToOneField) of your model are 485 optional. This is useful when you want to create base and related 486 instances in remote procedure. In this case primary key of base model is 487 not yet available. 488 489 """ 490 491 __abstract__ = True 492 493 494class ObjectNotFoundError(ResourceNotFoundError): 495 496 """Fault constructed from `model.DoesNotExist` exception.""" 497 498 def __init__(self, does_not_exist_exc): 499 """Construct fault with code Client.<object_name>NotFound.""" 500 message = str(does_not_exist_exc) 501 object_name = message.split()[0] 502 # we do not want to reuse initialization of ResourceNotFoundError 503 Fault.__init__( 504 self, faultcode='Client.{0}NotFound'.format(object_name), 505 faultstring=message) 506 507 508class ValidationError(BaseValidationError): 509 510 """Fault constructed from `ValidationError` exception.""" 511 512 def __init__(self, validation_error_exc): 513 """Construct fault with code Client.<validation_error_type_name>.""" 514 message = str(validation_error_exc) 515 # we do not want to reuse initialization of BaseValidationError 516 Fault.__init__( 517 self, faultcode='Client.{0}'.format( 518 type(validation_error_exc).__name__), faultstring=message) 519 520 521class DjangoService(Service): 522 523 """Service with common Django exception handling.""" 524 525 @classmethod 526 def call_wrapper(cls, ctx): 527 """Handle common Django exceptions.""" 528 try: 529 out_object = super(DjangoService, cls).call_wrapper(ctx) 530 except ObjectDoesNotExist as e: 531 raise ObjectNotFoundError(e) 532 except DjValidationError as e: 533 raise ValidationError(e) 534 return out_object 535 536 537# FIXME: To be removed in Spyne 3 538DjangoServiceBase = DjangoService 539