From 6f9fe0809b5afd2db0b4c3b20133eab6fb0709d0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 4 Jul 2016 16:38:28 +0100 Subject: [PATCH] Revert "Schemas & client libraries." --- docs/api-guide/schemas.md | 383 ------------------ docs/img/corejson-format.png | Bin 20499 -> 0 bytes docs/index.md | 7 - docs/topics/api-clients.md | 294 -------------- docs/tutorial/6-viewsets-and-routers.md | 26 +- .../7-schemas-and-client-libraries.md | 216 ---------- mkdocs.yml | 3 - requirements/requirements-optionals.txt | 1 - rest_framework/compat.py | 10 - rest_framework/filters.py | 20 - rest_framework/pagination.py | 14 - rest_framework/renderers.py | 17 +- rest_framework/routers.py | 43 +- rest_framework/schemas.py | 300 -------------- rest_framework/utils/encoders.py | 7 +- rest_framework/viewsets.py | 1 - runtests.py | 2 +- schema-support | 0 tests/test_routers.py | 2 +- tests/test_schemas.py | 137 ------- 20 files changed, 34 insertions(+), 1449 deletions(-) delete mode 100644 docs/api-guide/schemas.md delete mode 100644 docs/img/corejson-format.png delete mode 100644 docs/topics/api-clients.md delete mode 100644 docs/tutorial/7-schemas-and-client-libraries.md delete mode 100644 rest_framework/schemas.py delete mode 100644 schema-support delete mode 100644 tests/test_schemas.py diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md deleted file mode 100644 index 9fa1ba2e3..000000000 --- a/docs/api-guide/schemas.md +++ /dev/null @@ -1,383 +0,0 @@ -source: schemas.py - -# Schemas - -> A machine-readable [schema] describes what resources are available via the API, what their URLs are, how they are represented and what operations they support. -> -> — Heroku, [JSON Schema for the Heroku Platform API][cite] - -API schemas are a useful tool that allow for a range of use cases, including -generating reference documentation, or driving dynamic client libraries that -can interact with your API. - -## Representing schemas internally - -REST framework uses [Core API][coreapi] in order to model schema information in -a format-independent representation. This information can then be rendered -into various different schema formats, or used to generate API documentation. - -When using Core API, a schema is represented as a `Document` which is the -top-level container object for information about the API. Available API -interactions are represented using `Link` objects. Each link includes a URL, -HTTP method, and may include a list of `Field` instances, which describe any -parameters that may be accepted by the API endpoint. The `Link` and `Field` -instances may also include descriptions, that allow an API schema to be -rendered into user documentation. - -Here's an example of an API description that includes a single `search` -endpoint: - - coreapi.Document( - title='Flight Search API', - url='https://api.example.org/', - content={ - 'search': coreapi.Link( - url='/search/', - action='get', - fields=[ - coreapi.Field( - name='from', - required=True, - location='query', - description='City name or airport code.' - ), - coreapi.Field( - name='to', - required=True, - location='query', - description='City name or airport code.' - ), - coreapi.Field( - name='date', - required=True, - location='query', - description='Flight date in "YYYY-MM-DD" format.' - ) - ], - description='Return flight availability and prices.' - ) - } - ) - -## Schema output formats - -In order to be presented in an HTTP response, the internal representation -has to be rendered into the actual bytes that are used in the response. - -[Core JSON][corejson] is designed as a canonical format for use with Core API. -REST framework includes a renderer class for handling this media type, which -is available as `renderers.CoreJSONRenderer`. - -Other schema formats such as [Open API][open-api] (Formerly "Swagger"), -[JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can -also be supported by implementing a custom renderer class. - -## Schemas vs Hypermedia - -It's worth pointing out here that Core API can also be used to model hypermedia -responses, which present an alternative interaction style to API schemas. - -With an API schema, the entire available interface is presented up-front -as a single endpoint. Responses to individual API endpoints are then typically -presented as plain data, without any further interactions contained in each -response. - -With Hypermedia, the client is instead presented with a document containing -both data and available interactions. Each interaction results in a new -document, detailing both the current state and the available interactions. - -Further information and support on building Hypermedia APIs with REST framework -is planned for a future version. - ---- - -# Adding a schema - -You'll need to install the `coreapi` package in order to add schema support -for REST framework. - - pip install coreapi - -REST framework includes functionality for auto-generating a schema, -or allows you to specify one explicitly. There are a few different ways to -add a schema to your API, depending on exactly what you need. - -## Using DefaultRouter - -If you're using `DefaultRouter` then you can include an auto-generated schema, -simply by adding a `schema_title` argument to the router. - - router = DefaultRouter(schema_title='Server Monitoring API') - -The schema will be included at the root URL, `/`, and presented to clients -that include the Core JSON media type in their `Accept` header. - - $ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json - HTTP/1.0 200 OK - Allow: GET, HEAD, OPTIONS - Content-Type: application/vnd.coreapi+json - - { - "_meta": { - "title": "Server Monitoring API" - }, - "_type": "document", - ... - } - -This is a great zero-configuration option for when you want to get up and -running really quickly. If you want a little more flexibility over the -schema output then you'll need to consider using `SchemaGenerator` instead. - -## Using SchemaGenerator - -The most common way to add a schema to your API is to use the `SchemaGenerator` -class to auto-generate the `Document` instance, and to return that from a view. - -This option gives you the flexibility of setting up the schema endpoint -with whatever behaviour you want. For example, you can apply different -permission, throttling or authentication policies to the schema endpoint. - -Here's an example of using `SchemaGenerator` together with a view to -return the schema. - -**views.py:** - - from rest_framework.decorators import api_view, renderer_classes - from rest_framework import renderers, schemas - - generator = schemas.SchemaGenerator(title='Bookings API') - - @api_view() - @renderer_classes([renderers.CoreJSONRenderer]) - def schema_view(request): - return generator.get_schema() - -**urls.py:** - - urlpatterns = [ - url('/', schema_view), - ... - ] - -You can also serve different schemas to different users, depending on the -permissions they have available. This approach can be used to ensure that -unauthenticated requests are presented with a different schema to -authenticated requests, or to ensure that different parts of the API are -made visible to different users depending on their role. - -In order to present a schema with endpoints filtered by user permissions, -you need to pass the `request` argument to the `get_schema()` method, like so: - - @api_view() - @renderer_classes([renderers.CoreJSONRenderer]) - def schema_view(request): - return generator.get_schema(request=request) - -## Explicit schema definition - -An alternative to the auto-generated approach is to specify the API schema -explicitly, by declaring a `Document` object in your codebase. Doing so is a -little more work, but ensures that you have full control over the schema -representation. - - import coreapi - from rest_framework.decorators import api_view, renderer_classes - from rest_framework import renderers - - schema = coreapi.Document( - title='Bookings API', - content={ - ... - } - ) - - @api_view() - @renderer_classes([renderers.CoreJSONRenderer]) - def schema_view(request): - return schema - -## Static schema file - -A final option is to write your API schema as a static file, using one -of the available formats, such as Core JSON or Open API. - -You could then either: - -* Write a schema definition as a static file, and [serve the static file directly][static-files]. -* Write a schema definition that is loaded using `Core API`, and then - rendered to one of many available formats, depending on the client request. - ---- - -# API Reference - -## SchemaGenerator - -A class that deals with introspecting your API views, which can be used to -generate a schema. - -Typically you'll instantiate `SchemaGenerator` with a single argument, like so: - - generator = SchemaGenerator(title='Stock Prices API') - -Arguments: - -* `title` - The name of the API. **required** -* `patterns` - A list of URLs to inspect when generating the schema. Defaults to the project's URL conf. -* `urlconf` - A URL conf module name to use when generating the schema. Defaults to `settings.ROOT_URLCONF`. - -### get_schema() - -Returns a `coreapi.Document` instance that represents the API schema. - - @api_view - @renderer_classes([renderers.CoreJSONRenderer]) - def schema_view(request): - return generator.get_schema() - -Arguments: - -* `request` - The incoming request. Optionally used if you want to apply per-user permissions to the schema-generation. - ---- - -## Core API - -This documentation gives a brief overview of the components within the `coreapi` -package that are used to represent an API schema. - -Note that these classes are imported from the `coreapi` package, rather than -from the `rest_framework` package. - -### Document - -Represents a container for the API schema. - -#### `title` - -A name for the API. - -#### `url` - -A canonical URL for the API. - -#### `content` - -A dictionary, containing the `Link` objects that the schema contains. - -In order to provide more structure to the schema, the `content` dictionary -may be nested, typically to a second level. For example: - - content={ - "bookings": { - "list": Link(...), - "create": Link(...), - ... - }, - "venues": { - "list": Link(...), - ... - }, - ... - } - -### Link - -Represents an individual API endpoint. - -#### `url` - -The URL of the endpoint. May be a URI template, such as `/users/{username}/`. - -#### `action` - -The HTTP method associated with the endpoint. Note that URLs that support -more than one HTTP method, should correspond to a single `Link` for each. - -#### `fields` - -A list of `Field` instances, describing the available parameters on the input. - -#### `description` - -A short description of the meaning and intended usage of the endpoint. - -### Field - -Represents a single input parameter on a given API endpoint. - -#### `name` - -A descriptive name for the input. - -#### `required` - -A boolean, indicated if the client is required to included a value, or if -the parameter can be omitted. - -#### `location` - -Determines how the information is encoded into the request. Should be one of -the following strings: - -**"path"** - -Included in a templated URI. For example a `url` value of `/products/{product_code}/` could be used together with a `"path"` field, to handle API inputs in a URL path such as `/products/slim-fit-jeans/`. - -These fields will normally correspond with [named arguments in the project URL conf][named-arguments]. - -**"query"** - -Included as a URL query parameter. For example `?search=sale`. Typically for `GET` requests. - -These fields will normally correspond with pagination and filtering controls on a view. - -**"form"** - -Included in the request body, as a single item of a JSON object or HTML form. For example `{"colour": "blue", ...}`. Typically for `POST`, `PUT` and `PATCH` requests. Multiple `"form"` fields may be included on a single link. - -These fields will normally correspond with serializer fields on a view. - -**"body"** - -Included as the complete request body. Typically for `POST`, `PUT` and `PATCH` requests. No more than one `"body"` field may exist on a link. May not be used together with `"form"` fields. - -These fields will normally correspond with views that use `ListSerializer` to validate the request input, or with file upload views. - -#### `encoding` - -**"application/json"** - -JSON encoded request content. Corresponds to views using `JSONParser`. -Valid only if either one or more `location="form"` fields, or a single -`location="body"` field is included on the `Link`. - -**"multipart/form-data"** - -Multipart encoded request content. Corresponds to views using `MultiPartParser`. -Valid only if one or more `location="form"` fields is included on the `Link`. - -**"application/x-www-form-urlencoded"** - -URL encoded request content. Corresponds to views using `FormParser`. Valid -only if one or more `location="form"` fields is included on the `Link`. - -**"application/octet-stream"** - -Binary upload request content. Corresponds to views using `FileUploadParser`. -Valid only if a `location="body"` field is included on the `Link`. - -#### `description` - -A short description of the meaning and intended usage of the input field. - - -[cite]: https://blog.heroku.com/archives/2014/1/8/json_schema_for_heroku_platform_api -[coreapi]: http://www.coreapi.org/ -[corejson]: http://www.coreapi.org/specification/encoding/#core-json-encoding -[open-api]: https://openapis.org/ -[json-hyperschema]: http://json-schema.org/latest/json-schema-hypermedia.html -[api-blueprint]: https://apiblueprint.org/ -[static-files]: https://docs.djangoproject.com/en/dev/howto/static-files/ -[named-arguments]: https://docs.djangoproject.com/en/dev/topics/http/urls/#named-groups diff --git a/docs/img/corejson-format.png b/docs/img/corejson-format.png deleted file mode 100644 index 36c197a0d02a78fa165f3cd123343ca87a27f102..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20499 zcmZU(V{~P~vpyV7Y}>Xbwrx8nwv&l%+nHoyV`5KiYhs%x=9{^{`@i?D^_~xB?cH6~ zwX3>&Rn=1!siYu@0E-I?0s?{{EhVM`0s_kR^?U&R?dxAq3V8(t1e?rCR8&b?RFqiB z*}>e()(iwhD>U^7q?Y<*%g1?_qSNtZlEjI|iG7hUGUA5Qv4&U*GIB9S3NvzFQ4xx; zm^iu^3WeC9n2;LzCFR}g#??pur|%`-^n{;wqxmFnTVd2uzvm!d_)j;G|;DjrA{!mCf>T+r2uPDg|VYHs=Ba^1}Qb%Plf$7KsR$4EaUTeEw`47kI>xJ2k-2hL|Z>zR0qBF;H4eNy2xTr?;)$u7_ z-;QeE=v^UrC<%l{C_qp%u4U>VH9+knX7ATK8=V4HX)s&w$K`HfF?tz`oykw z3ZaSnRT7FUra`0E(pXLTgW3ZgE(~1CXn!`<(qv(_iRS0rrz!SL`kJ8k^VRcrt0hZFFmnC2qaMRgLs^@jf@iyZ-(YY+UC z_>(;;^~9EaK*{O95O+mHa56sEI7jyhaG(1Qep}Kb+-l+aG!!_4h!{!o>h&4|v?}Cm zpvpH+=-6iz(6+)62l;;aSbYZQa|UjPX%W7Ykrg;qPr!pXJrij=a)7vvAK$w_5oON6 zfoS!BI_^|>LvTCGhOJuG1wdQQCifwF27n-oFO$Qe_YwXCbqz#ehFaEUJj#_A|-;dT3`>NxRXhyTzxV(LMs_j5S%ufuGGhws2Vk-ZUs zLR0(+x+X)7U@eTzAd8E@5|KbnbR{REk+e=UUszU5fZ0d8g$wRyHhE%3we-O?vSq%^ zP?961L{p7vizpvn@8j!}GdX0o(Xyt;a;I|VTtc>qzaNq?R;_U5IBnf{^6~mn8DKcXQSJ2Kls}RBAaoN5hUE8s zk}IKl!Zbke2a%CPB*=|WsiFOa77g(T*+O?k7eVi!nV{jLHlc2yp{2>9Ayl4FdZU~} z6OX76vneFHL9S2Gl07HsCTCPqQMvr7Ro$RjOc*;fQj}StrM*sEmZ|{ z1$_ma8iCq$Ic8aTd1yID89{k}d1D!qvWIF<;fHj;6qz(YYEL*>OjSfxkxdL3niixH zp^^LmVn=T$aDnhw@^}6*^>N6_?-ME4NLLD1dDrRV?s>Mk@tN#-s5y%{&H1fg{?h># z$mTO<*G5vtWk$#58RlZxkl0N)8#qX88mte@Q_MRVVj1w6ZyBr^HO%~`SpajRG>c}l zICDm0d$T+vVXY8tcx^DP2JM>0m^%7K=SJ%WuZEKPr3S?M52HQPc_W~ytVJaN&s^91 z2(WDSXd&3Q*()#=xPyGOGK?~)JT}{Z6PX<8f>g?D!E@v`g{8-w&XrD~iLVK{46^KD zPiXIZiF`?VX@9B6N6&}L=f_v-`Pb9@2hNX0Pv0MkH_br5ZNTCDKHN6len&s&@bsWV zFHxU=+$YKB6#*%)tvmTu*%}L@5V{iPH7Xay3uqDrc=+b~c>9$ge^+F%;Ik3}O)BUz57(xqZE z2RjEkmpTVvg=dvu9c7(l1vcz9TsBZNC?1p@-0dgr;~sR6UZp&yFr^Zy8>um?b5+a~ z*Hx4iNoiE6=cuEqIji$j-^pc4uZnQ1wkbkZ78WNJ%@y9-wCrA9Z!CNGZ;;gw`zzk( zJ$j2jcwJVn^H#TurWScAxQWCnPO4v2{SD#i;gRmr^OX@5>KEPX9O9K{kr$lAon+FP z(=n~q)e-#dVMAr$&RP**=>&j_~;H#&`AP+jIHR`Oc;Qt5B{$Hbpu$JN1a@h!4#*b1HHs;@q|q z<<{gZc@b-;2~ewumqJSDqGw|v&sQt>E#rA_3P z?~||C{kMCtd+QDUZS2kODf`v%+7PJ@t_+SGqW#-qVu5ka?aq+(=oVsa7{;%QR$;asJ<^opw3Srygp?$97p2ebLD2!xk8nbAS*jySCcn5g&=;CDp)x1EEWB^1i?TQg+y@v?b+=k843X}VQUN-=?Yx0eUwN9h>IaHO!5u=22Y z(GbzjQ5sRcQQ@+`Wlj}baus;r_hN=K3shp~{<2;+#55cB z1eA}r!q!OWy*p0c{~dALJJaV(aah5;xU-tCaNm`(6j_t`%&oDkn5SnOBw z>GKkOoStpS(TzBSv+)v$5f zv87D+l=?@#e({iFuPOa3@a&9)oYe}b3L;E*egN6~1_@F|0AeHtBW=@7DdsI`K|#=6;Vpzs8WaaOublERZeCVWm$TBfP;!7nPH}-qP4WkH1Op9<{v%!JYC^#~=)oanEsZehIm$RvO6^RoURicdxMEYvQ*c>)qfDpdt-z^t zRUH_;g9h+T&NhEL)W!s+c~_%W>Dv1J9bJXuO5u&;Ldi)*@@(60Z%#7bz~M9Ho9ue) zLV87iF?g`KxkAE5@Ikv{@T5RjG>6}VJ&G}kBA2KU^NsMsoyXQi$HH;QBhI-76j%Sp$}%j6sP{8g)Nn*PHj{zHx-)sVBUVp=Xw&QZ35 z!B4djO)+{MdOv+u$M#n_my^_nhQ`bz((zncc3Ka$K6TY2UdEiR4A;?Le_tAmV}~#f zbTWDBE8mt^YE<v0At}gFsGPqnbLfZJ`Ea++Ss3e?U#Nsl^#58O>59C zFLS=v!i^JSn_|vKPWFfH>BBsrYlxA_kX86DT(W2W&UF>UDZ=s1pv~B-AFCT|@Nj8x zes*aAI{#u}X8cL#DXS$!uIsA3?tdf;gEmTO1(RvQjr&X<%1wcFu(_e)9raLC{-f;C z64sK!I{sAVr1&%*HwsT2Z#gR_TRDqPxAj7{v8u_-^nEeyuYGA(UT`H~;sd`_=I907 zLqANvRN8RE?+1ut4puQ|EK)90Fj2CgDu^ht9Wo0x;s?$xi)+j}o;yP)F)5?7e~Jvd z?48eq(fML)&eJvoP%M*tsHiv?5YCg#gg>PaDX^Pn?WzXfg-132cV5wAz>Ei9bJS$| zC6_;M-mj4FaYXu474gTnXIRL$(1AbCg|oY9Ne%O(7V9!i;}Fv((^GkPG*^|DHC2@r ze1<+d1B1WPcPoOKI+nEbl&di+0cE;vFAsnB&g_nT-7Yo!eBDJQ021>zhwE$N3>lkMkXUVNpTea?6mc6He&!SzD-cV+Qa7a!91wUZ<^|Aff zSl_BF?v{WRSX4u#27hDdI!*lU-|y!m@sitA3+6E$ip(u7x;uUEYA$`3Y`39)#s48% z7T|x@dk%e5cSB@beq^xQGy-jz=|1k{dgQ--{X17q)F3$IpY}n@#QWB)d!s$6XS4Ot z1LBI|E_9rDmhrAd+6*H(M2E~AA`pdN2vtm&3f}~RfW-h#fWVIj&h!gEnT9R1IK?f3 zU{~qjZg=e9a{qZ^xUaUU-;h|dPWiYA8uF;}x$~?}EN@@DobtpklVx?tZtoqu}}#FLbPMq&x7b%w z^ZG4ury?g$PZP&+sr*(CQ5~{4uqMp{rH;r*WG$`0SCtu5F8BV1scsQ0uHuW}w^taVqgR zS)nU=8jM$n*O*0{t!po6_dF+IiFK@W=6W7=D)8gTdb)Qb-uL`5<|TQCbLCC6O>#DP zLv%tBEPoRZ#XpUs{qB#;`Ml?$O>rcXDuM&-R-TtXu>bUEfx9vEXYt_HIo`+Ci{QFM zU%{%CJtxR_Sr9D(FycK%5K=nJJZXsDAY2d|3=qXr@Fz9;fT@#DUwgYXM)k3E@l-r|s&9WZK!VN`9wW}Iyvf3&@QybHg( zdw_d5bx?nBaHPMDw_UrLNP$dpOj|@bK?W8%5%CbMkHNH(**cEa1$j{1T2wzL+aMGt zlP4-H=MFfH8O$5udo>)8qz zXrzwEu36c6Iv4vDJW{aKpD0PLx<$yFY@aRd-6{Bb9{H49)aeU(C|*O6X%RMEk@l!5 zZctPkSN%iR!uR5Gvg=-6-rM1^ncLmx%HT5f5(_88+tq2rV#SgDviO~RSnk|*?Wn>5 z9!@Y_#IJ<@?9;DPriZeAs^N#f(DCv&LM2d%UdWI@cqPc8vd4EJ{1FHXA%bHtvTZU5 zVcW8+dlF4*%(37a5%3xMnD9^|{RJS6BZd!_U%dH)N(2XLbdI>oQ!V!Q7)(lJ)rgb6 z3BYsR;R^Y=`Z;=5G~OQK4z8n%Crc-9H~eQ(uk?x{m#C*aiPVm)Q|d03a(0Gz&QGI?E*41K09T3_&B{C+nH?xAm#>Lmu|bca@H&>EyR z+}X5ia1Ypsj0;;IdbWCxt;wEho_;sYH^n!+mkF3ZQJ?6!s9i-{rO36}l_r$e9ZSa; zonQ#1ztIG-Cm5s_r|c!AtIcS*s=;bbX_T+}`cG4-^Kgi}QN>zLl2(7e(AIb7cX@{o z1|{21ZxGX(XcNK|N0eaACUCslsYO;!SJOtuZ4bV3u1-9PS z-@?HbgDXU_MF1nU{cdCxzp#JVnB4D{prR(NAR5Ro@$z{M@Vb3y>G9+^-Eo;Mwc$6S zol!X)LZU}t_)*vJ5~x;qq+P5ZMRllSV6ifq8BX=67|`lNvv_uT7e2*~c|XS^byxYE>8R<;yk)lJw4t_^v|sqs z|Ade`UQLIv?!|8n#!f((3jTf#KOxGVz-0>ZLs)VK=m+Hm4lq_;fkq63+jd{ri;3Vt zjmVK8BtFCDjzK!4Kjcn>=|IX3_6-6Ia_`YP8D7&qslBNNA(Lb=3y}z+NV3SKP~?$* z6p||=DqR#*6k`?&NcJnbx)RKD9C93H8_m0 zE#)gTD$@GNURl`H;lpIWA!nxp=s8riW6SmS`ulPb))9*3__Vl!cqBbM<3K|F11rYkVI+SIwqBPQia=d*?zr3uBw#V@!(gwdzj4> zCo`wbsU5EH{J|PxHM(63Z%{`_ySLZ+Wz9Xt!}$7@@AmceyV28LDwVY2vczuwyC>n> z?uBgW&2X(7m37Is)i2Ifw%{x3t+Rc(md`zOH(Sp|3CJ*cu3Xn=21cQ-LS zhyYf=BHGzm&-n7PCf$tb9t8*isBX`NZMV%a2p4}(;LBh*(#I=j+9#MTOz$`|>R@8V=xOKpRZai_;q&DAdbBfhH7547v$c2O@#H7{FAAQo z=YPXYq{RP4;%dWBswJ;PEb8EFM$Ey;&d5wE0830v%;#)s&Z8nG@!#xUZ~UZ|uC9(e zOiUgg9*iDrj1JBgOf1~o+)T`@OsuR7Ula^3KkQwNJsIp>$o{j)|651Q%mv_V<>+eV zU{CySU1JjmH&=dA(tm>f=l7rQH1o9jUrF{Z|E<=Sf=vHrm{=H@nf_<(FIK*PqdZDh zo@TaMVpeu$_AXyC1lX84`TmRk|2Ol$68{gU_WyFSvU2}l&i^y>-<*6*{}lK?3jN1g z|26t$E&*6RrvEX$0PLJE2m%PmcQ9!&VKqUT}0DzqqjQAjrt#lY_<=Zbs!@i{mBC`WiXRXC-j zvJ#Floo;N!cXr*L6BZWs1;4PIoJsD1bPD5E?qO zSdw3w5fW6G7y?YhoCE~bL0AY`3JD@)V(wQSdHNR>G8iaHBQi*23o$Wt5!4saiLzpn z>=zYuU_gR4bijZvGBU&*_!rS#8P#a<7gdOm&!iNQR8qG;MB?EE5d^BG3^f0VcpP z(V1&q(eD<`cC`W0q3a%V)Bn>qyg0hmlZ@GN=4XJkBk<7Z1C-3Xe)#TWzTWjjMK1e0 zgEhLTmBhk~I8mb5!+x@&1e1^nB|)H(DTOJf@BR5oJ(;aYBa=d!z4=|MBmuBv>)n3A ziK37=VHDa2n7tL#?kQrfEQ;kjkpF&eF)Nvt5hV1D%<<`Zr%aa06qCahRr$-lITFNa zHvkg0QX8_+{r4q@cg(}d>~JU|ALfF#EOskY`=0lQ)}kMu?~j0i2o$G8g6J8~H10GX z7DkrWw_<0v~N3baBw$o!0El_HGAN}!=F&Nr$1QT0sP(%PwP3Y z#Px4qEKnU&K1P0TdqjIR9hZM<{oieT&H+R&q%aJ@W`8?8m?UCxLpM4+_HKrWJmlQD zg1CSa&L91PZ~Xz=3MrYUPpP#&ke_#opCQ3eNT*xlOg%B7a9D^^OnOiz5BZg&Cevi< zN8YVxL_Z*>zbfbcQQj9g^7l)mpqQNGj(j4Yh9s^f_yYHLIIyvg+0NKJwaF0wzf9naxt5X zS}a2!6!PCh@5hpsmX^H)9_xY6z@xLwyOu3Kyrb#-WmzeyAp!z|?&?@Z!|s@9ET&zo zo~O{MexmnTqMh^A#+5b~s#_q?X?GyfvJqq1vw@wg=K2N8elg=MJYuq`zFye_DDZxw zxVNZjxcKMehUgPTwM1?fA=3e1Z{{-2&~l7aSl*OK{PCRssofKs|Kkwn{reGWFgO&V z(Zl(w08akPLMgN92r@yw^T*}Y2jb_w;b-XQYtQF=)=DhrW<*{dVdvwf_lvOC%Sq`? zQ?0N3u>VddmTT7H;^NB=OYuIqo`FFP5#?dDbI;3SEw01cN&W{-8k1oS2lQd2j)jX0 zTld=mlT8r7Yr#B2(Y;)IiF3AFtaEiA{t{npd;7oz2n;xQ);M;Y77Q|)S5uwlL>sB}o8%)rTmxq>~ z*qgoKvun9@=e4!9CiB&$rLc~xZrwd#FF4XMv*F8J`W-IaFOozbEyOsYSR6KqQs0+5 ztLxB#^r0hxNAB2R1kQDk!+i374jm2Aw=x>EPY#=I#X)<230HL)h+jxjR-? zg3I7?BK3P7Hw?G1qg=nFxX5!_fRXi)A*+6Jfy994p!K*42l zY3E&@7NWOps4VnIESM>4sox9d%??{Nx#j!~Yf{zGI+sZlvcGv*%+hmL(BkdZnvFNw z-87VWs*TQ0Umz8BW92}|QQkZ$-UrYWjpj?_v)ET0k0vsI5zz}wu={K)=JJGARM5Ul zd@i+g7ZaIuk3JLM22zYBf=uV~mJ%J0jmf+RIEn1ke(7FsIZN94dfBlJJ~jzkF3zkE z{3&q1pz0M9Su`3RfrQtg@cH2Ixm3Hd;PNF97L~5qwz1n1xo+gS)5t#hDO&>|x?(^ea zXI`(~B$SZN#A_i&3-cw}Qp$5kNwy&^b1(7hS~M~u!Q3|HzgKlr(oAM?$l9cx|4o!b z$W+1v4%u@%nLd}1sLGQ4`1lgrJ;m9SfaL$w&%>XV`*+cRBW&b4W?0y-!}vowihTid zb#XtQ3D3kH9@zgqo$uP?C*lz}kLQ2Lpu`Y} z7)nU7TzNkL7pfk5(S{ifaJbtWj<=bNGA_TF%wQ3<5yqEm#!bRRXaW;3jpw_K@DnOq ztd}+AAcsDTEi#915&$COa-!l-9?$-2lJ`eVW|_dFB9y9`V3MA&jyVLhM-Nd-&ngiD zTWnWZ6DDHnFKKPY%-*V+ZvcPE;Qi~`ZI&v}VV)k=H;Gl#G`R0%kBZmO6knQrsGTJ;Xt_hQT`aJGBkX>x(C%$MQ zrfC1SDJM*K-WU?l86`TCJ@dy<2g|tl`q#d&DMq*^boU)gDbsUYl|5+Fvilrvmv1gd zQ;;3D8g{({JD%u7VhRd%-sAS!B;U}6?6ZBJs*`aRMx>;|Af&rB0JnB-jRspDTfXfb z(zlu1E|h*(kA8~dq=Y-TqWDYN>rQ(^C&!eAACxFnx9=#qi}_|$%1*X%8?s9%(z$Nt z9Os9PHRZzdb%f>W4RjMM(`w&+`a*yJoT?o$HrWH6SW*Xaa>i(CI=Xpsv5N?#moLX4 z&JgQhE}(V(lCZy$`xf2v1h=97{B;jKsuP`TCoCazMt8RbIH(bS0tEYiUam}7CR^GSbM$RJ#^YdMCV-7OnQ$bp;OcpKm1^T*k{$i1f z9~gYXkAAJ}d7+d4MveqnfjlAyry_pCaaWCm8irZrps-57UTxZcqHsbq<&u@^PnW6- z{Z(@Q<-e9T6y2vYzcp!B?hHVNLvXGBaZ+jqkNf3R4kbNRalj(qy*?ou>t_qMn;ep8c4K61nb*F@^>Frfw8#M0e$@{#WUobFYTtAi049h(l&_UU&a zz#=kb%N~JB+~fB7RmRl}Xab(XXJ69V%hkR(X{nE!EC>eUb+Hb5jD>EMe>F?_{k)u%1h zjSiNGV@O`9Q9ZYkxhifQF~F8GG-%HX6M2s!hiAH)NoC#fMx7m|@48gJ$jl!G@KyER ziw^@7SE-8nRY{guakv6n))zFX?dEVfGncQ=Mftw3W;?`^HK1uc zV7H#G!(mIne-ou6v8p_l#WyYWZ$?k(P#<)-sgPad^CK4Lt{h~9(8M1a-thr@f#s;g zb(IaV4tCKIdKivuYF3~odlC*-n=?p$WBLs{{`rv>x`h~b96BQRAe*4V7^1j&-&^j4 zB;{CC+C))yW@I&qRHhVlPKn@;Thj!kDNki^Q_WaeLDtBT9AU{KuGj!ea|JTouufBJ zb7ND*ppZZz=E>-d38N3=i0M<}S!?qz15{?mq8lKO;D5!>tfh>_6Hgmg|-N_XRh>mIFjm+35A(mxJ3hye)5r%f;CwSyQOXh?4 zx+sJXNzN(I*YgB^$W!tp&jhqspSNga1}-}Jyc^E<0tkZ1yLslWM@QT-!)^}iZwdLJ zPTu6W_DdsI%CYW)leBG+NP9OOv;V@jC8AZJENuIYq_$kP^K&_tpm!l&3qNYI=hx?= z#bY? zl}EY3#VPH8KLcZj>@-a{6b`61os*h)_*4m5cO@Hk+!qeBcP5LkS>@LUrdKn!g>%&90@BbTFYob*boAl zv>yitM;7|3*}+uXdTop@z=e+RY^niEzZGjQw6dMe1Dv06uW{35!F`+WvQ&xN>$D@%B&xIRmb}_&}hqy~t=J z$fe{E3x~U5TQREL<^1&CkyJ{UT6q(TT?rMX7X;}Jn9YiCg~;o7%y?#B*d%*%qZzQB z{uW&B_sNVjibsIZ(2Me_ZZM8h3YVy;JsmF(mM?nEd5a=BB+ z!qwfZY!+vc00=2T8@?e5X%)OZ(1E3X4Xz!b-95&3{9k*UPWf;9RQGb!dy$d&pmE}1 zfb)J;5pzE!I582wpl@!lF>AE&9L$f*PQ-R)b zEvtLX-K!CEwzyWNItR~df2masa7#KPn}1QT;jqcN%1e7Wi4xD)H|C7)$8QKRjvHyt zW234Q19n&6!Q0mQM}19DkXdEvHwkkl zI8Cv3K97bEHW!Gns7R$tdUM2r&r2hm@cD{*1G#DVwOWPzf?SAA`7RR|G?}H{bHv%; z!qT41@WBcqXjA-$K$C*_DW`w=~@;W-QnZ{Q@646ENCnVgqt# zJf|JF2aA}%8Z5v2o+&sJl7W#k!dQK+3ZCQak> z*nZKd1}0Y15Uee(I41u`3_T)Nz2(TiQQwPl)DRu-Bn^kJ3+Gx*Z~mr6xBA_{yD#_> zAgz0Q^s>M0;4?cSBb8#Nn^)lTPQhH53v4)Qp=rn2v?6OKNZ`j|8A1%)yrrI%*?6+$ zQ2X`sZ9Gxd9L4r-lQ0s>?{muim0rjAp8udtI&9OQXKrA zsKoj@$`tow^P@aMEbA~5_mZ$!U)*MsXDQRkZI@d_fy0haX_ulo!`7FbU{o0;%LZc8 z>&WuAqiiP^X~K-(W?OVROA)cF2X)-RB-iZ13=dOhzSBWewUA7Ik@+FN9nE*`&6QK; zjI(8LxA;O?0v%Gydt@o<%wmHZ3Q3&Efb@|vX>K$0)WBRvH>q4zQt`O{FQ>x|Ult)N z0Jbo~o@g~Wk?GVcEJLUI$x%Sj6Osh(|3&aDD!$GN5hxtBkVDdVMl6i`PA|GWT4%yl z8g7Bjtb+d!_+t78d{uUnxBLUXK)%imNN+^rKi~@s%!<3ADiMNl+N{=vj0c-NPtKo@Y?&BkwJx(ap&YnXZ93m zzOEuihZ_clQS&Opsk6xy#=(?-7fRjph{)&uiu}9tji>oRH%u*{IDRuMC=OBfNqNi; zY;2hN$EhrNO9$R0)TYRVjeUOST`P9DI&c_pi=BtOSvfWvx|H@y%YVCy>2)!`9S(y` zJy#FN{ex-2`*(|OH6(DL>GgKAxNh@Ru=ZCslDU81fIRXHbP;4GgAOw|M{)m)y7p*> zPf9I8G>+g4Q5Bz1ElOF22q~^*MM;e4VmBJK%EushmoMYRa!sN4*rd#m_&JB^_ZoEG zJT>xit_5qED2ehrp6l(y1R-u*}bvz;@ZIsa?(L#76g#-0hnVTZ;U zV&Ybhfv?|YbPZ?7#CgU@mp_C`-URKD+#~Yp;`qw_9%j|7gRiwr^36&Lp3?mZ)U1;c z()Y?+w{i!8f(Xm`rT*iQX;CU?=Dj#1<2YDNzbpBtDYl;8S)by-+{>Z!>9k)miHbnk zp5A!^A(HmlrKE86miT2Unq1*br(M;(nc>b zIXxF zHwHMmE&f~;Nv(>jtvSq6Y=!^u+|>Nr#BbcJ*mxr{DYC`nq*WuIh)Jfz2YVbTr}xEg zLtZg~^g(qtcuA9AOk#OxXqnmSPjJRBcD%Fx%g978e=L4KIEhNzrYlIBI zH7*tmVoREJFW^^zHqypU<5M!L{1bKD%)yurNPKfuAME<_JT8&J6XE%80n#%xXMBbu z!I&hLVC6EYkzbkmgPWQg8Sc~66Pc=U_jqN)_nh(s>8TPK@lkC^d!ab?zwrg*OEeXK zHrpdvze35cb`&NOir6cM@b`8;;9mOM1<*+-<^`;_6dJbt20ke-yppvB&JKzrLq`f( zCucs+qiEU(NH6~?opry*OxaKikm)?4IZwc=yP-p(b;haMx`XTw3s75jJG9;3;N-W= z1xHkVWs@If0+f)JH1H1f{R)N;^WFkoz~YbL76;Vr%{o6K}J+o#Z;`cR3{3^eTPv`M~z4B>_9$1XFyhWLXuzek~qptr==`jq$r($6M z#!daHg)?RsYU%$=H!+h)RCIh5$#0ss#6qvnOkm0QAa$S9E!6G2dEWlyBCJtu%dBjakgbpXakU)hu;xO z%5Tnc&J^jd`Xh#Ta#Jf`adGwP8)8Yd`ktVw{czBcLCZvqmx|u!Ih|H5W95}PTTx5= zpM708B|ckhT64LfA-eRs{R6d2lY4k-B+C6?30nGRi%f|@#!=@BvPIK(Wvg3gN?(Oy zlLBwTnu3qe0W_TX%^N|%QjUO`kP&uzt5+({B{VCOSR!<8j?KkqZH}K+>`ZY^2-w(6 zUBVu^?F*v&Y%&0?h8GNer7M@{+rnqPmQz$j)vRR(q48d*T3d1=Ephi#k_(48lga5Q zrN?@_>w$E8(eUX;5WH?zrm@=G*~XvuP&&%B7AAp^KntTAf_^2v0hT`_qx9=u(409; zNlq5-peGGyx7XS!!cqZW@Kf3EUBP1kWmIvj2XB0AFguH9iRO7m*v zQUSxg(a_4Ce!H>#tvscT(M%5HX2#lqgUN7`MV$jOwBD;7x?Z_UN7r`NLk5YmOQN0kxmJ;e&4XT60Ij?-+J0h=sOnJ2n?M@P+Rsfmb>Hn zOU^E8?Ib55MgLdBNjgBQ>DYl!=>GE@G3W?D@U@RygTZlc376&UGm*Bb(7?qT@* z#r>CRY`iv9gH8U%ua^KMBLyn~%F4?U3sbp4<+Dv(10QvUl_qzXMY&U}=+5Nun@LWI zlLNA$`)50*f^(;72%*&h(iHi-m$Leh~aAecP zUy?#}nNKwCH#PFn-enlzuv@Q5tY?kI)b`x1ckFhQiuuc7!e7!FjR#;b#U^|G_9@Y6 z#7p)XvU($gH`P6;47RugwM_m_`+-0Z0IJc9QvMuBGznbd^28BHHc$b6{FMH9qS<_( zSoru>KiA=R}HhV9P<1&zDFt=bW;kUAdca%y9qc51Ia|5Od` zO_7Ft)9G5-=T(V`X`^G$Ub4}W@iA7F)~VG!c!wpj4iP9ujBEs|Nt%?#{#DcyLUu$1 zC1E?NLzn(n?-Tn9;gX=c$^TU!r$~rR5AoATrN5&5q=>JK0kto^=&RV-%l$=^)2~AQ zuMUWc@)cXgs&zyeT)|u~x}yq99Ti1d>>$kge@mS*Q>jL7oyOd%kf!ZOm6o1PCy=&KGW1deomHi8Qa{bXolhY7cyM!J zO}$GtgSPpqh*Hzj4=^{WcRB&As+M zHinL_cxiJvas*Ctp4EJ8_rc_%HDIdNG(vzjx6GF+{%UQ-CjX90m#AGEENpUwVAa+) zGo~bnn)zIBj8Wbn2>G{yVfENwpddr_*91`Xt^=o+>ozgPDi*JW~u zTmo5&9qb%Zufu(k$*Dej^a01-V}4(soh?`lb5(;Bjdi>*O0mAM;X<3EQLB+EOS=lX zMA}ExXy`kF^a#B?Ei-td)nDRh3HeyN`3$KhR)&5tOkOsWy6?zoGYOfCQxi$D?Cx_Ng6E^Wg9)0hD=5#T8p)n$vbdD0-6}I9xQD!=e-CjRz%DTkjIGqz5BPjT-n&l zg66BJCFmN>*ES`p{R(Pa;t9N&)G|Vz@_8SHMdn6J#N_&!iw1}0S5GvEk{?fz#=O0~ z9dGwnTSev;o6#Pp_2UnCAB+_u>1wPZukXYHF6MtPYRM$`utOiH>Wbcwau@fe(~(5DT_ZjLbHN!w)1cend8Z;%;S z*&smu?Go)18FNxu8sV*w%Qa$Zn!im{W`$>E4W71ud9QVRGp)D#bYGCmjVj;V+I2(V z0w63dhF#DdKEEARfAyrX=&y80j~aAAt}Qb>gq8nEd%w_tan2S0hm9(mtwoQukfe*^Pb;FKS2i}1hs8z;^dHVIpB$cmHjPq^=~6WLLfby|kiIo^WJ8dDNq znb0i`1~s0YIUoOxO!SLXd0my>V0m~)wBL`@eAZA%W)i(n3Tib4#&kIzC2DR=u4vYXSq4Fy2?Rc-K!S0ZLiK1wd}0*b67UI&w8^o zKUXMAR#pSPyvFwhXV|p*N;@pf+LgFNGCNBL2T|Zhx+|kTBw?X*YRWH_HrtQREEqc* z$=Aeh^aickZ4(mWnUcC}17_#w^d+V=fzTrv+F*TCV@34fzu(7dhh%l|Z0eQXW6Je( z!mYP7!6>xu|46i2F!VLFHS!6=(kVY~lq#LaCX_;rcLFSiOHU#Cv-KREN`G;!jei*EfA5E`8l@~=}TMdAz3EUuJfrTTL4JEX+X=KcR^ z=n41*J)+PU_lM@sKYs0hFQXy`a~S{O8TyYHu>d^oTdww8JKu0kXx5;B_n&AaY*uLh z0tXc65eOj(HGnymh=Ta^`HNeHtH@WymB#sn#IeX2ZpmRIMzosnNcyyHYf*7hH1bie z^&w1N>p=ySFnb}2qbZhW=DcaGuCQW)s zl3;JXq~Xg4cPZgg&EsP_r&6_{6&`DoUf9u%>4Ihdm@BdATd$o9TBnVYiD$DIc{pAv zd-FE-tkv=v!vv@aGEo!!4mE%KcVb!-Q!ZU8m}bfJ*X~2OTpn8yDSpDKjM6qA3+B zEzHg^fddm0YR{ZQ%#r-r1?qy4X#7BHyLJQSezh9ge!vIm^7Evb>i`1_EcdjCGCEri z_G{k~yiXU}9Q$Qj*+sWs?$3CJySlozIW9Ig3GeRicKWKK)}_z^>umlJMvjl4z#*0wjibw15c z&*`T-sEBI9fyHDX=I7^EWwH>wBj?8|9+x}8sMi``-2YY5JDWBA|4KL$f2h9yk4Mx9 z*|$MRmh241mMzL|>}$e|vG02n$vO=tVhnw@(SQjrGc+>C(DdbbGF^|M&zlD^vl7+Qrh&uSOVo7t@ zJ}-41mOWZM;@p&C;pTApbi8e8#(x{Npg>aH`9&$bDV-4@72g|>GL<^%ad4X*!*tI7 z{HXY~+1`3qsMYK=TPAx{XK3c=MKz*SAT}?;d z4yYZ}^%AEh4_58BxI2e@4S3NC{_W!M&k&(EO3MfSEcu9rb-ERfMQtz7z+?UF+4!AUQ}6+H8aCv%xtaQ zL|~ZFWn=5drzpw%gYtOQTGMCWZ;Fl)8I1oM|K~wFRw#q!u;dpt#eBbAhM-6a+5gf< z4Y)G^HQn~;lJ9x#xvHMMoptpI+b8lX)2rGHMYzDFF5$z@2X-~1A4ZWoq(zpAwDcJ@ z=ecq8Z*8ZaoA~(^3+v#xh2U)dFb~j@^N8* z%v56>cesY)>*IH38~wIs|1KtRQ50;p1FYOqSp}h<-BphGyBf?(xpC*?Qp!7aFVy1@r^9{BtgbwpUl14T-0B;9Flu;qu?A3^UCH>eJ2I5io=IgUi)F*~fCE?QP5>gU00 z;m@5(3eQ)TP?~D_(e;%>)s^)u(mRed#=;S7^9!>d7pmVoR3JTg%%&aUPoi=Yqb*02 zo+aizFz1{0GPi8caY!U^M9x7MtwQHK4y1BuZw<#Xo~QA(M~Cao`L|IySTYOeeeM@y zLvo5o!m|D6Awx5!QAdN1GRlfDEKY4@eddT;ITg5^n=tze(WzW}q14DZr|ibhwg@D` zuXXPaxHQWKR>9eOSfu@mqkvd&!O0`XpJ&R3nk^Kfdx$#BHrd2tri(AK*C-~tYoxqO zz5@|@B4fxvrw7_>Kb;tO#~zSAaToN<6rYxM-)N0%eb-cBz6~fN#E0e6WiyG+#kuXS zf01oYaO3d^2za+564ZOTUC?KeeNZ+bJrjH`el{`Y$5D_C%o%@G&EJ+(Eiw0-W0g|2 zJb@Fu`fYn*=0)r~D`L##OS84U7kzj8hVf+bD+s6!Wn%*pT5Q+**2aM=s~cJWls%gB zEkBkATWbd`_*1#gjaIl7PS}FF{*)A`kiT6`O{+-vAMCuZgKykdrOTUBd4=wMd~^bk$BJ)VWZ$#} zsFCWzn@?otJVV~_rN3V&+Hi0}H8^^~GX%}m6+hrduKXQ*5$){Jt>CKBmsTI0kbKtb zTxX?~w8H1ZsV;XQxgKc;5Av`aS`>T{>)`?Y8t7DC%H*E;B6FWso!bWkRXso1=UYQX z1_uW#bjLRzvFX11w)Wxvff3};K#VWkK%b*Xj}zkG?vB!-nq}{^%sHx@ajxOS=Vzub zMYVcHHomNp|8^5$(Zh)4WS0}n*@P?~n6%XbJbipgJ?rUZP}H5E-eR@DC`WTyzVeWG z2Q7r#0lx&mBO7|D30o05rvVG0r)(;~Pd$VN%ZHI#3{{yNfE3)k3fNZM&m^J&ZfA1R z5uIm^_*5V!nY46~e>T8dSEo{h2cTJ5fKV5h2sDZ}(zNK#*T?dQLXpk6+C7psfZ|yb zUyb_3rNS(dz;qoLds*zAuu4Qkgis!ZLmWYoepYvVxb)a(Ih|E79pXF9x8lw4kl||B zG1cYG24lWz@I-N^9<-sWD^LuQREx+G6da~`cA=ki(11G?kU&V&ffq#sMyu0xc5&PP z#3}_$*FG@fWqs%}BzE_l^8`61P@Vq}9u8ca58uBx>^z!XqFt{ma$`i(=e{3w*seW) z0~*fQaXDugAwoXqe;3)VP=67iN-y;;8FAqmQ~K$}E=qs(_o;gXOofAE#a8ou0r(#QFWJUkg!6 z`&wj4&?(BWTJT2bs}J%eYR@n@o@|y*ep4;hRQZa84s`gdugzAyBNy>$f zL)I|G4vx)%Vnol+h}u&Bp!9JQ+bFnrWw6Omh_AE|2cN8zR2&pyH!#d@t&xmV?`x(M zlTK`So3ex7r;2OC_{E6&WCTWA5G5v;K$tK3?0Yt%_F><3Sf9Z#8}|Z?Gd46lZ@#qs z9xLo)9Kd2TK^Y?}|A;9$_`+Mqq4JoUpvpSd=LH(#?rsD2anmO#o#;^#w-B#x73ip#Kbt z41T&RloW!`@ci=gF&9~$XR}EC_R_uJgP`T%`&A^2OI4c;jCT&OpOFIN^~fOZw_d?!2ehNnNi^al^(>IP9)0QnW59 z(^Z0SjH!|dH`XV{RhUL#Y;ftnVFa?h=31fNWY`l$aF!|~lOlkq6n%P{96d+QO9kX| zj|gCa=@WsQik;_JmHF!_`Lrubu-<%Z=D?~b>M7Ugd?Syq7(~hyMJ8)8q{t(;&hrlI zxi=5{>aC@-aVywu&4&X$EoHo-dw}R2XL|^9MRdJ0ZR1cer~jOXK#F4m*zVVF_17)7 zV$I3J*Ad1!z@*oBfJ8vmd&MkfpaIDaP&S%rRb(2Lt7urZ2VAMNbp9nEonM3D)&v4$ z>}l!zOI_C#KnM}~V;7SeU8;SMFsHaVqP8`mUC_CXP!<#=yJ-CUb}w z7w<5vJuMF~N|R3lo*c9d$T9#@2g(}eGBla)3j|(gY0l&WX72$$kP7*qN$}7v1qcBC z@C(j8z$5B=vtvdCxE>L!^1MJ{eAAxV<>`wHGjH*t>^j|(AnyLs1g#;a!dThSzanKB W6-VWcKNo4ud8WozM*kXmru+}qf?_iO diff --git a/docs/index.md b/docs/index.md index 072d80c66..cc8d183f4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -91,7 +91,6 @@ REST framework requires the following: The following packages are optional: -* [coreapi][coreapi] (1.21.0+) - Schema generation support. * [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API. * [django-filter][django-filter] (0.9.2+) - Filtering support. * [django-crispy-forms][django-crispy-forms] - Improved HTML display for filtering. @@ -215,7 +214,6 @@ The API guide is your complete reference manual to all the functionality provide * [Versioning][versioning] * [Content negotiation][contentnegotiation] * [Metadata][metadata] -* [Schemas][schemas] * [Format suffixes][formatsuffixes] * [Returning URLs][reverse] * [Exceptions][exceptions] @@ -228,7 +226,6 @@ The API guide is your complete reference manual to all the functionality provide General guides to using REST framework. * [Documenting your API][documenting-your-api] -* [API Clients][api-clients] * [Internationalization][internationalization] * [AJAX, CSRF & CORS][ajax-csrf-cors] * [HTML & Forms][html-and-forms] @@ -299,7 +296,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [redhat]: https://www.redhat.com/ [heroku]: https://www.heroku.com/ [eventbrite]: https://www.eventbrite.co.uk/about/ -[coreapi]: http://pypi.python.org/pypi/coreapi/ [markdown]: http://pypi.python.org/pypi/Markdown/ [django-filter]: http://pypi.python.org/pypi/django-filter [django-crispy-forms]: https://github.com/maraujop/django-crispy-forms @@ -322,7 +318,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [tut-4]: tutorial/4-authentication-and-permissions.md [tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md [tut-6]: tutorial/6-viewsets-and-routers.md -[tut-7]: tutorial/7-schemas-and-client-libraries.md [request]: api-guide/requests.md [response]: api-guide/responses.md @@ -344,7 +339,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [versioning]: api-guide/versioning.md [contentnegotiation]: api-guide/content-negotiation.md [metadata]: api-guide/metadata.md -[schemas]: 'api-guide/schemas.md' [formatsuffixes]: api-guide/format-suffixes.md [reverse]: api-guide/reverse.md [exceptions]: api-guide/exceptions.md @@ -353,7 +347,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [settings]: api-guide/settings.md [documenting-your-api]: topics/documenting-your-api.md -[api-clients]: topics/api-clients.md [internationalization]: topics/internationalization.md [ajax-csrf-cors]: topics/ajax-csrf-cors.md [html-and-forms]: topics/html-and-forms.md diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md deleted file mode 100644 index 5f09c2a8f..000000000 --- a/docs/topics/api-clients.md +++ /dev/null @@ -1,294 +0,0 @@ -# API Clients - -An API client handles the underlying details of how network requests are made -and how responses are decoded. They present the developer with an application -interface to work against, rather than working directly with the network interface. - -The API clients documented here are not restricted to APIs built with Django REST framework. - They can be used with any API that exposes a supported schema format. - -For example, [the Heroku platform API][heroku-api] exposes a schema in the JSON -Hyperschema format. As a result, the Core API command line client and Python -client library can be [used to interact with the Heroku API][heroku-example]. - -## Client-side Core API - -[Core API][core-api] is a document specification that can be used to describe APIs. It can -be used either server-side, as is done with REST framework's [schema generation][schema-generation], -or used client-side, as described here. - -When used client-side, Core API allows for *dynamically driven client libraries* -that can interact with any API that exposes a supported schema or hypermedia -format. - -Using a dynamically driven client has a number of advantages over interacting -with an API by building HTTP requests directly. - -#### More meaningful interaction - -API interactions are presented in a more meaningful way. You're working at -the application interface layer, rather than the network interface layer. - -#### Resilience & evolvability - -The client determines what endpoints are available, what parameters exist -against each particular endpoint, and how HTTP requests are formed. - -This also allows for a degree of API evolvability. URLs can be modified -without breaking existing clients, or more efficient encodings can be used -on-the-wire, with clients transparently upgrading. - -#### Self-descriptive APIs - -A dynamically driven client is able to present documentation on the API to the -end user. This documentation allows the user to discover the available endpoints -and parameters, and better understand the API they are working with. - -Because this documentation is driven by the API schema it will always be fully -up to date with the most recently deployed version of the service. - ---- - -# Command line client - -The command line client allows you to inspect and interact with any API that -exposes a supported schema format. - -## Getting started - -To install the Core API command line client, use `pip`. - - $ pip install coreapi - -To start inspecting and interacting with an API the schema must first be loaded -from the network. - - - $ coreapi get http://api.example.org/ - - snippets: { - create(code, [title], [linenos], [language], [style]) - destroy(pk) - highlight(pk) - list([page]) - partial_update(pk, [title], [code], [linenos], [language], [style]) - retrieve(pk) - update(pk, code, [title], [linenos], [language], [style]) - } - users: { - list([page]) - retrieve(pk) - } - -This will then load the schema, displaying the resulting `Document`. This -`Document` includes all the available interactions that may be made against the API. - -To interact with the API, use the `action` command. This command requires a list -of keys that are used to index into the link. - - $ coreapi action users list - [ - { - "url": "http://127.0.0.1:8000/users/2/", - "id": 2, - "username": "aziz", - "snippets": [] - }, - ... - ] - -To inspect the underlying HTTP request and response, use the `--debug` flag. - - $ coreapi action users list --debug - > GET /users/ HTTP/1.1 - > Accept: application/vnd.coreapi+json, */* - > Authorization: Basic bWF4Om1heA== - > Host: 127.0.0.1 - > User-Agent: coreapi - < 200 OK - < Allow: GET, HEAD, OPTIONS - < Content-Type: application/json - < Date: Thu, 30 Jun 2016 10:51:46 GMT - < Server: WSGIServer/0.1 Python/2.7.10 - < Vary: Accept, Cookie - < - < [{"url":"http://127.0.0.1/users/2/","id":2,"username":"aziz","snippets":[]},{"url":"http://127.0.0.1/users/3/","id":3,"username":"amy","snippets":["http://127.0.0.1/snippets/3/"]},{"url":"http://127.0.0.1/users/4/","id":4,"username":"max","snippets":["http://127.0.0.1/snippets/4/","http://127.0.0.1/snippets/5/","http://127.0.0.1/snippets/6/","http://127.0.0.1/snippets/7/"]},{"url":"http://127.0.0.1/users/5/","id":5,"username":"jose","snippets":[]},{"url":"http://127.0.0.1/users/6/","id":6,"username":"admin","snippets":["http://127.0.0.1/snippets/1/","http://127.0.0.1/snippets/2/"]}] - - [ - ... - ] - -Some actions may include optional or required parameters. - - $ coreapi action users create --params username example - -## Authentication & headers - -The `credentials` command is used to manage the request `Authentication:` header. -Any credentials added are always linked to a particular domain, so as to ensure -that credentials are not leaked across differing APIs. - -The format for adding a new credential is: - - coreapi credentials add - -For instance: - - coreapi credentials add api.example.org "Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" - -The optional `--auth` flag also allows you to add specific types of authentication, -handling the encoding for you. Currently only `"basic"` is supported as an option here. -For example: - - coreapi credentials add api.example.org tomchristie:foobar --auth basic - -You can also add specific request headers, using the `headers` command: - - coreapi headers add api.example.org x-api-version 2 - -For more information and a listing of the available subcommands use `coreapi -credentials --help` or `coreapi headers --help`. - -## Utilities - -The command line client includes functionality for bookmarking API URLs -under a memorable name. For example, you can add a bookmark for the -existing API, like so... - - coreapi bookmarks add accountmanagement - -There is also functionality for navigating forward or backward through the -history of which API URLs have been accessed. - - coreapi history show - coreapi history back - -For more information and a listing of the available subcommands use -`coreapi bookmarks --help` or `coreapi history --help`. - -## Other commands - -To display the current `Document`: - - coreapi show - -To reload the current `Document` from the network: - - coreapi reload - -To load a schema file from disk: - - coreapi load my-api-schema.json --format corejson - -To remove the current document, along with all currently saved history, -credentials, headers and bookmarks: - - coreapi clear - ---- - -# Python client library - -The `coreapi` Python package allows you to programatically interact with any -API that exposes a supported schema format. - -## Getting started - -You'll need to install the `coreapi` package using `pip` before you can get -started. Once you've done so, open up a python terminal. - -In order to start working with an API, we first need a `Client` instance. The -client holds any configuration around which codecs and transports are supported -when interacting with an API, which allows you to provide for more advanced -kinds of behaviour. - - import coreapi - client = coreapi.Client() - -Once we have a `Client` instance, we can fetch an API schema from the network. - - schema = client.get('https://api.example.org/') - -The object returned from this call will be a `Document` instance, which is -the internal representation of the interface that we are interacting with. - -Now that we have our schema `Document`, we can now start to interact with the API: - - users = client.action(schema, ['users', 'list']) - -Some endpoints may include named parameters, which might be either optional or required: - - new_user = client.action(schema, ['users', 'create'], params={"username": "max"}) - -## Codecs - -Codecs are responsible for encoding or decoding Documents. - -The decoding process is used by a client to take a bytestring of an API schema -definition, and returning the Core API `Document` that represents that interface. - -A codec should be associated with a particular media type, such as **TODO**. - -This media type is used by the server in the response `Content-Type` header, -in order to indicate what kind of data is being returned in the response. - -#### Configuring codecs - -The codecs that are available can be configured when instantiating a client. -The keyword argument used here is `decoders`, because in the context of a -client the codecs are only for *decoding* responses. - -In the following example we'll configure a client to only accept `Core JSON` -and `JSON` responses. This will allow us to receive and decode a Core JSON schema, -and subsequently to receive JSON responses made against the API. - - from coreapi import codecs, Client - - decoders = [codecs.CoreJSONCodec(), codecs.JSONCodec()] - client = Client(decoders=decoders) - -#### Loading and saving schemas - -You can use a codec directly, in order to load an existing schema definition, -and return the resulting `Document`. - - schema_definition = open('my-api-schema.json', 'r').read() - codec = codecs.CoreJSONCodec() - schema = codec.load(schema_definition) - -You can also use a codec directly to generate a schema definition given a `Document` instance: - - schema_definition = codec.dump(schema) - output_file = open('my-api-schema.json', 'r') - output_file.write(schema_definition) - -## Transports - -Transports are responsible for making network requests. The set of transports -that a client has installed determines which network protocols it is able to -support. - -Currently the `coreapi` library only includes an HTTP/HTTPS transport, but -other protocols can also be supported. - -#### Configuring transports - -The behaviour of the network layer can be customized by configuring the -transports that the client is instantiated with. - - import requests - from coreapi import transports, Client - - credentials = {'api.example.org': 'Token 3bd44a009d16ff'} - transports = transports.HTTPTransport(credentials=credentials) - client = Client(transports=transports) - -More complex customizations can also be achieved, for example modifying the -underlying `requests.Session` instance to [attach transport adaptors][transport-adaptors] -that modify the outgoing requests. - -[heroku-api]: https://devcenter.heroku.com/categories/platform-api -[heroku-example]: http://www.coreapi.org/tools-and-resources/example-services/#heroku-json-hyper-schema -[core-api]: http://www.coreapi.org/ -[schema-generation]: ../api-guide/schemas.md -[transport-adaptors]: http://docs.python-requests.org/en/master/user/advanced/#transport-adapters diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 00152cc17..f1dbe9443 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -130,7 +130,27 @@ Using viewsets can be a really useful abstraction. It helps ensure that URL con That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views instead of function based views. Using viewsets is less explicit than building your views individually. -In [part 7][tut-7] of the tutorial we'll look at how we can add an API schema, -and interact with our API using a client library or command line tool. +## Reviewing our work -[tut-7]: 7-schemas-and-client-libraries.md +With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, and comes complete with authentication, per-object permissions, and multiple renderer formats. + +We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views. + +You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox]. + +## Onwards and upwards + +We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start: + +* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests. +* Join the [REST framework discussion group][group], and help build the community. +* Follow [the author][twitter] on Twitter and say hi. + +**Now go build awesome things.** + + +[repo]: https://github.com/tomchristie/rest-framework-tutorial +[sandbox]: http://restframework.herokuapp.com/ +[github]: https://github.com/tomchristie/django-rest-framework +[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework +[twitter]: https://twitter.com/_tomchristie diff --git a/docs/tutorial/7-schemas-and-client-libraries.md b/docs/tutorial/7-schemas-and-client-libraries.md deleted file mode 100644 index 8d772a5bf..000000000 --- a/docs/tutorial/7-schemas-and-client-libraries.md +++ /dev/null @@ -1,216 +0,0 @@ -# Tutorial 7: Schemas & client libraries - -A schema is a machine-readable document that describes the available API -endpoints, their URLS, and what operations they support. - -Schemas can be a useful tool for auto-generated documentation, and can also -be used to drive dynamic client libraries that can interact with the API. - -## Core API - -In order to provide schema support REST framework uses [Core API][coreapi]. - -Core API is a document specification for describing APIs. It is used to provide -an internal representation format of the available endpoints and possible -interactions that an API exposes. It can either be used server-side, or -client-side. - -When used server-side, Core API allows an API to support rendering to a wide -range of schema or hypermedia formats. - -When used client-side, Core API allows for dynamically driven client libraries -that can interact with any API that exposes a supported schema or hypermedia -format. - -## Adding a schema - -REST framework supports either explicitly defined schema views, or -automatically generated schemas. Since we're using viewsets and routers, -we can simply use the automatic schema generation. - -You'll need to install the `coreapi` python package in order to include an -API schema. - - $ pip install coreapi - -We can now include a schema for our API, by adding a `schema_title` argument to -the router instantiation. - - router = DefaultRouter(schema_title='Pastebin API') - -If you visit the API root endpoint in a browser you should now see `corejson` -representation become available as an option. - -![Schema format](../img/corejson-format.png) - -We can also request the schema from the command line, by specifying the desired -content type in the `Accept` header. - - $ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json - HTTP/1.0 200 OK - Allow: GET, HEAD, OPTIONS - Content-Type: application/vnd.coreapi+json - - { - "_meta": { - "title": "Pastebin API" - }, - "_type": "document", - ... - -The default output style is to use the [Core JSON][corejson] encoding. - -Other schema formats, such as [Open API][openapi] (formerly Swagger) are -also supported. - -## Using a command line client - -Now that our API is exposing a schema endpoint, we can use a dynamic client -library to interact with the API. To demonstrate this, let's use the -Core API command line client. We've already installed the `coreapi` package -using `pip`, so the client tool should already be installed. Check that it -is available on the command line... - - $ coreapi - Usage: coreapi [OPTIONS] COMMAND [ARGS]... - - Command line client for interacting with CoreAPI services. - - Visit http://www.coreapi.org for more information. - - Options: - --version Display the package version number. - --help Show this message and exit. - - Commands: - ... - -First we'll load the API schema using the command line client. - - $ coreapi get http://127.0.0.1:8000/ - - snippets: { - highlight(pk) - list() - retrieve(pk) - } - users: { - list() - retrieve(pk) - } - -We haven't authenticated yet, so right now we're only able to see the read only -endpoints, in line with how we've set up the permissions on the API. - -Let's try listing the existing snippets, using the command line client: - - $ coreapi action snippets list - [ - { - "url": "http://127.0.0.1:8000/snippets/1/", - "highlight": "http://127.0.0.1:8000/snippets/1/highlight/", - "owner": "lucy", - "title": "Example", - "code": "print('hello, world!')", - "linenos": true, - "language": "python", - "style": "friendly" - }, - ... - -Some of the API endpoints require named parameters. For example, to get back -the highlight HTML for a particular snippet we need to provide an id. - - $ coreapi action snippets highlight --param pk 1 - - - - - Example - ... - -## Authenticating our client - -If we want to be able to create, edit and delete snippets, we'll need to -authenticate as a valid user. In this case we'll just use basic auth. - -Make sure to replace the `` and `` below with your -actual username and password. - - $ coreapi credentials add 127.0.0.1 : --auth basic - Added credentials - 127.0.0.1 "Basic <...>" - -Now if we fetch the schema again, we should be able to see the full -set of available interactions. - - $ coreapi reload - Pastebin API "http://127.0.0.1:8000/"> - snippets: { - create(code, [title], [linenos], [language], [style]) - destroy(pk) - highlight(pk) - list() - partial_update(pk, [title], [code], [linenos], [language], [style]) - retrieve(pk) - update(pk, code, [title], [linenos], [language], [style]) - } - users: { - list() - retrieve(pk) - } - -We're now able to interact with these endpoints. For example, to create a new -snippet: - - $ coreapi action snippets create --param title "Example" --param code "print('hello, world')" - { - "url": "http://127.0.0.1:8000/snippets/7/", - "id": 7, - "highlight": "http://127.0.0.1:8000/snippets/7/highlight/", - "owner": "lucy", - "title": "Example", - "code": "print('hello, world')", - "linenos": false, - "language": "python", - "style": "friendly" - } - -And to delete a snippet: - - $ coreapi action snippets destroy --param pk 7 - -As well as the command line client, developers can also interact with your -API using client libraries. The Python client library is the first of these -to be available, and a Javascript client library is planned to be released -soon. - -For more details on customizing schema generation and using Core API -client libraries you'll need to refer to the full documentation. - -## Reviewing our work - -With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, includes a schema-driven client library, and comes complete with authentication, per-object permissions, and multiple renderer formats. - -We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views. - -You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox]. - -## Onwards and upwards - -We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start: - -* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests. -* Join the [REST framework discussion group][group], and help build the community. -* Follow [the author][twitter] on Twitter and say hi. - -**Now go build awesome things.** - -[coreapi]: http://www.coreapi.org -[corejson]: http://www.coreapi.org/specification/encoding/#core-json-encoding -[openapi]: https://openapis.org/ -[repo]: https://github.com/tomchristie/rest-framework-tutorial -[sandbox]: http://restframework.herokuapp.com/ -[github]: https://github.com/tomchristie/django-rest-framework -[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework -[twitter]: https://twitter.com/_tomchristie diff --git a/mkdocs.yml b/mkdocs.yml index b10fbefb5..19d1b3553 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,7 +20,6 @@ pages: - '4 - Authentication and permissions': 'tutorial/4-authentication-and-permissions.md' - '5 - Relationships and hyperlinked APIs': 'tutorial/5-relationships-and-hyperlinked-apis.md' - '6 - Viewsets and routers': 'tutorial/6-viewsets-and-routers.md' - - '7 - Schemas and client libraries': 'tutorial/7-schemas-and-client-libraries.md' - API Guide: - 'Requests': 'api-guide/requests.md' - 'Responses': 'api-guide/responses.md' @@ -42,7 +41,6 @@ pages: - 'Versioning': 'api-guide/versioning.md' - 'Content negotiation': 'api-guide/content-negotiation.md' - 'Metadata': 'api-guide/metadata.md' - - 'Schemas': 'api-guide/schemas.md' - 'Format suffixes': 'api-guide/format-suffixes.md' - 'Returning URLs': 'api-guide/reverse.md' - 'Exceptions': 'api-guide/exceptions.md' @@ -51,7 +49,6 @@ pages: - 'Settings': 'api-guide/settings.md' - Topics: - 'Documenting your API': 'topics/documenting-your-api.md' - - 'API Clients': 'topics/api-clients.md' - 'Internationalization': 'topics/internationalization.md' - 'AJAX, CSRF & CORS': 'topics/ajax-csrf-cors.md' - 'HTML & Forms': 'topics/html-and-forms.md' diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 54c080491..241e1951d 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -2,4 +2,3 @@ markdown==2.6.4 django-guardian==1.4.3 django-filter==0.13.0 -coreapi==1.21.1 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 9c69eaa03..dd30636f4 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -156,16 +156,6 @@ except ImportError: crispy_forms = None -# coreapi is optional (Note that uritemplate is a dependancy of coreapi) -try: - import coreapi - import uritemplate -except (ImportError, SyntaxError): - # SyntaxError is possible under python 3.2 - coreapi = None - uritemplate = None - - # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS # Fixes (#1712). We keep the try/except for the test suite. guardian = None diff --git a/rest_framework/filters.py b/rest_framework/filters.py index caff1c17f..fdd9519c6 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -72,9 +72,6 @@ class BaseFilterBackend(object): """ raise NotImplementedError(".filter_queryset() must be overridden.") - def get_fields(self, view): - return [] - class DjangoFilterBackend(BaseFilterBackend): """ @@ -131,17 +128,6 @@ class DjangoFilterBackend(BaseFilterBackend): template = loader.get_template(self.template) return template_render(template, context) - def get_fields(self, view): - filter_class = getattr(view, 'filter_class', None) - if filter_class: - return list(filter_class().filters.keys()) - - filter_fields = getattr(view, 'filter_fields', None) - if filter_fields: - return filter_fields - - return [] - class SearchFilter(BaseFilterBackend): # The URL query parameter used for the search. @@ -231,9 +217,6 @@ class SearchFilter(BaseFilterBackend): template = loader.get_template(self.template) return template_render(template, context) - def get_fields(self, view): - return [self.search_param] - class OrderingFilter(BaseFilterBackend): # The URL query parameter used for the ordering. @@ -347,9 +330,6 @@ class OrderingFilter(BaseFilterBackend): context = self.get_template_context(request, queryset, view) return template_render(template, context) - def get_fields(self, view): - return [self.ordering_param] - class DjangoObjectPermissionsFilter(BaseFilterBackend): """ diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 6ad10d860..a66c7505c 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -157,9 +157,6 @@ class BasePagination(object): def get_results(self, data): return data['results'] - def get_fields(self, view): - return [] - class PageNumberPagination(BasePagination): """ @@ -283,11 +280,6 @@ class PageNumberPagination(BasePagination): context = self.get_html_context() return template_render(template, context) - def get_fields(self, view): - if self.page_size_query_param is None: - return [self.page_query_param] - return [self.page_query_param, self.page_size_query_param] - class LimitOffsetPagination(BasePagination): """ @@ -412,9 +404,6 @@ class LimitOffsetPagination(BasePagination): context = self.get_html_context() return template_render(template, context) - def get_fields(self, view): - return [self.limit_query_param, self.offset_query_param] - class CursorPagination(BasePagination): """ @@ -717,6 +706,3 @@ class CursorPagination(BasePagination): template = loader.get_template(self.template) context = self.get_html_context() return template_render(template, context) - - def get_fields(self, view): - return [self.cursor_query_param] diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index e313998d1..7ca680e74 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -22,8 +22,7 @@ from django.utils import six from rest_framework import VERSION, exceptions, serializers, status from rest_framework.compat import ( - INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi, - template_render + INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, template_render ) from rest_framework.exceptions import ParseError from rest_framework.request import is_form_media_type, override_method @@ -791,17 +790,3 @@ class MultiPartRenderer(BaseRenderer): "test case." % key ) return encode_multipart(self.BOUNDARY, data) - - -class CoreJSONRenderer(BaseRenderer): - media_type = 'application/vnd.coreapi+json' - charset = None - format = 'corejson' - - def __init__(self): - assert coreapi, 'Using CoreJSONRenderer, but `coreapi` is not installed.' - - def render(self, data, media_type=None, renderer_context=None): - indent = bool(renderer_context.get('indent', 0)) - codec = coreapi.codecs.CoreJSONCodec() - return codec.dump(data, indent=indent) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index a71bb7791..70a1149ab 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -22,11 +22,9 @@ from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import NoReverseMatch -from rest_framework import exceptions, renderers, views +from rest_framework import views from rest_framework.response import Response from rest_framework.reverse import reverse -from rest_framework.schemas import SchemaGenerator -from rest_framework.settings import api_settings from rest_framework.urlpatterns import format_suffix_patterns Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs']) @@ -257,7 +255,6 @@ class SimpleRouter(BaseRouter): lookup=lookup, trailing_slash=self.trailing_slash ) - view = viewset.as_view(mapping, **route.initkwargs) name = route.name.format(basename=basename) ret.append(url(regex, view, name=name)) @@ -273,13 +270,8 @@ class DefaultRouter(SimpleRouter): include_root_view = True include_format_suffixes = True root_view_name = 'api-root' - schema_renderers = [renderers.CoreJSONRenderer] - def __init__(self, *args, **kwargs): - self.schema_title = kwargs.pop('schema_title', None) - super(DefaultRouter, self).__init__(*args, **kwargs) - - def get_api_root_view(self, schema_urls=None): + def get_api_root_view(self): """ Return a view to use as the API root. """ @@ -288,33 +280,10 @@ class DefaultRouter(SimpleRouter): for prefix, viewset, basename in self.registry: api_root_dict[prefix] = list_name.format(basename=basename) - view_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) - schema_media_types = [] - - if schema_urls and self.schema_title: - view_renderers += list(self.schema_renderers) - schema_generator = SchemaGenerator( - title=self.schema_title, - patterns=schema_urls - ) - schema_media_types = [ - renderer.media_type - for renderer in self.schema_renderers - ] - class APIRoot(views.APIView): _ignore_model_permissions = True - renderer_classes = view_renderers def get(self, request, *args, **kwargs): - if request.accepted_renderer.media_type in schema_media_types: - # Return a schema response. - schema = schema_generator.get_schema(request) - if schema is None: - raise exceptions.PermissionDenied() - return Response(schema) - - # Return a plain {"name": "hyperlink"} response. ret = OrderedDict() namespace = request.resolver_match.namespace for key, url_name in api_root_dict.items(): @@ -341,13 +310,15 @@ class DefaultRouter(SimpleRouter): Generate the list of URL patterns, including a default root view for the API, and appending `.json` style format suffixes. """ - urls = super(DefaultRouter, self).get_urls() + urls = [] if self.include_root_view: - view = self.get_api_root_view(schema_urls=urls) - root_url = url(r'^$', view, name=self.root_view_name) + root_url = url(r'^$', self.get_api_root_view(), name=self.root_view_name) urls.append(root_url) + default_urls = super(DefaultRouter, self).get_urls() + urls.extend(default_urls) + if self.include_format_suffixes: urls = format_suffix_patterns(urls) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py deleted file mode 100644 index cf84aca74..000000000 --- a/rest_framework/schemas.py +++ /dev/null @@ -1,300 +0,0 @@ -from importlib import import_module - -from django.conf import settings -from django.contrib.admindocs.views import simplify_regex -from django.core.urlresolvers import RegexURLPattern, RegexURLResolver -from django.utils import six - -from rest_framework import exceptions, serializers -from rest_framework.compat import coreapi, uritemplate -from rest_framework.request import clone_request -from rest_framework.views import APIView - - -def as_query_fields(items): - """ - Take a list of Fields and plain strings. - Convert any pain strings into `location='query'` Field instances. - """ - return [ - item if isinstance(item, coreapi.Field) else coreapi.Field(name=item, required=False, location='query') - for item in items - ] - - -def is_api_view(callback): - """ - Return `True` if the given view callback is a REST framework view/viewset. - """ - cls = getattr(callback, 'cls', None) - return (cls is not None) and issubclass(cls, APIView) - - -def insert_into(target, keys, item): - """ - Insert `item` into the nested dictionary `target`. - - For example: - - target = {} - insert_into(target, ('users', 'list'), Link(...)) - insert_into(target, ('users', 'detail'), Link(...)) - assert target == {'users': {'list': Link(...), 'detail': Link(...)}} - """ - for key in keys[:1]: - if key not in target: - target[key] = {} - target = target[key] - target[keys[-1]] = item - - -class SchemaGenerator(object): - default_mapping = { - 'get': 'read', - 'post': 'create', - 'put': 'update', - 'patch': 'partial_update', - 'delete': 'destroy', - } - - def __init__(self, title=None, patterns=None, urlconf=None): - assert coreapi, '`coreapi` must be installed for schema support.' - - if patterns is None and urlconf is not None: - if isinstance(urlconf, six.string_types): - urls = import_module(urlconf) - else: - urls = urlconf - patterns = urls.urlpatterns - elif patterns is None and urlconf is None: - urls = import_module(settings.ROOT_URLCONF) - patterns = urls.urlpatterns - - self.title = title - self.endpoints = self.get_api_endpoints(patterns) - - def get_schema(self, request=None): - if request is None: - endpoints = self.endpoints - else: - # Filter the list of endpoints to only include those that - # the user has permission on. - endpoints = [] - for key, link, callback in self.endpoints: - method = link.action.upper() - view = callback.cls() - view.request = clone_request(request, method) - try: - view.check_permissions(view.request) - except exceptions.APIException: - pass - else: - endpoints.append((key, link, callback)) - - if not endpoints: - return None - - # Generate the schema content structure, from the endpoints. - # ('users', 'list'), Link -> {'users': {'list': Link()}} - content = {} - for key, link, callback in endpoints: - insert_into(content, key, link) - - # Return the schema document. - return coreapi.Document(title=self.title, content=content) - - def get_api_endpoints(self, patterns, prefix=''): - """ - Return a list of all available API endpoints by inspecting the URL conf. - """ - api_endpoints = [] - - for pattern in patterns: - path_regex = prefix + pattern.regex.pattern - - if isinstance(pattern, RegexURLPattern): - path = self.get_path(path_regex) - callback = pattern.callback - if self.should_include_endpoint(path, callback): - for method in self.get_allowed_methods(callback): - key = self.get_key(path, method, callback) - link = self.get_link(path, method, callback) - endpoint = (key, link, callback) - api_endpoints.append(endpoint) - - elif isinstance(pattern, RegexURLResolver): - nested_endpoints = self.get_api_endpoints( - patterns=pattern.url_patterns, - prefix=path_regex - ) - api_endpoints.extend(nested_endpoints) - - return api_endpoints - - def get_path(self, path_regex): - """ - Given a URL conf regex, return a URI template string. - """ - path = simplify_regex(path_regex) - path = path.replace('<', '{').replace('>', '}') - return path - - def should_include_endpoint(self, path, callback): - """ - Return `True` if the given endpoint should be included. - """ - if not is_api_view(callback): - return False # Ignore anything except REST framework views. - - if path.endswith('.{format}') or path.endswith('.{format}/'): - return False # Ignore .json style URLs. - - if path == '/': - return False # Ignore the root endpoint. - - return True - - def get_allowed_methods(self, callback): - """ - Return a list of the valid HTTP methods for this endpoint. - """ - if hasattr(callback, 'actions'): - return [method.upper() for method in callback.actions.keys()] - - return [ - method for method in - callback.cls().allowed_methods if method != 'OPTIONS' - ] - - def get_key(self, path, method, callback): - """ - Return a tuple of strings, indicating the identity to use for a - given endpoint. eg. ('users', 'list'). - """ - category = None - for item in path.strip('/').split('/'): - if '{' in item: - break - category = item - - actions = getattr(callback, 'actions', self.default_mapping) - action = actions[method.lower()] - - if category: - return (category, action) - return (action,) - - # Methods for generating each individual `Link` instance... - - def get_link(self, path, method, callback): - """ - Return a `coreapi.Link` instance for the given endpoint. - """ - view = callback.cls() - - fields = self.get_path_fields(path, method, callback, view) - fields += self.get_serializer_fields(path, method, callback, view) - fields += self.get_pagination_fields(path, method, callback, view) - fields += self.get_filter_fields(path, method, callback, view) - - if fields and any([field.location in ('form', 'body') for field in fields]): - encoding = self.get_encoding(path, method, callback, view) - else: - encoding = None - - return coreapi.Link( - url=path, - action=method.lower(), - encoding=encoding, - fields=fields - ) - - def get_encoding(self, path, method, callback, view): - """ - Return the 'encoding' parameter to use for a given endpoint. - """ - # Core API supports the following request encodings over HTTP... - supported_media_types = set(( - 'application/json', - 'application/x-www-form-urlencoded', - 'multipart/form-data', - )) - parser_classes = getattr(view, 'parser_classes', []) - for parser_class in parser_classes: - media_type = getattr(parser_class, 'media_type', None) - if media_type in supported_media_types: - return media_type - # Raw binary uploads are supported with "application/octet-stream" - if media_type == '*/*': - return 'application/octet-stream' - - return None - - def get_path_fields(self, path, method, callback, view): - """ - Return a list of `coreapi.Field` instances corresponding to any - templated path variables. - """ - fields = [] - - for variable in uritemplate.variables(path): - field = coreapi.Field(name=variable, location='path', required=True) - fields.append(field) - - return fields - - def get_serializer_fields(self, path, method, callback, view): - """ - Return a list of `coreapi.Field` instances corresponding to any - request body input, as determined by the serializer class. - """ - if method not in ('PUT', 'PATCH', 'POST'): - return [] - - fields = [] - - serializer_class = view.get_serializer_class() - serializer = serializer_class() - - if isinstance(serializer, serializers.ListSerializer): - return coreapi.Field(name='data', location='body', required=True) - - if not isinstance(serializer, serializers.Serializer): - return [] - - for field in serializer.fields.values(): - if field.read_only: - continue - required = field.required and method != 'PATCH' - field = coreapi.Field(name=field.source, location='form', required=required) - fields.append(field) - - return fields - - def get_pagination_fields(self, path, method, callback, view): - if method != 'GET': - return [] - - if hasattr(callback, 'actions') and ('list' not in callback.actions.values()): - return [] - - if not hasattr(view, 'pagination_class'): - return [] - - paginator = view.pagination_class() - return as_query_fields(paginator.get_fields(view)) - - def get_filter_fields(self, path, method, callback, view): - if method != 'GET': - return [] - - if hasattr(callback, 'actions') and ('list' not in callback.actions.values()): - return [] - - if not hasattr(view, 'filter_backends'): - return [] - - fields = [] - for filter_backend in view.filter_backends: - fields += as_query_fields(filter_backend().get_fields(view)) - return fields diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index e5b52ea5f..f883b4925 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -13,7 +13,7 @@ from django.utils import six, timezone from django.utils.encoding import force_text from django.utils.functional import Promise -from rest_framework.compat import coreapi, total_seconds +from rest_framework.compat import total_seconds class JSONEncoder(json.JSONEncoder): @@ -64,9 +64,4 @@ class JSONEncoder(json.JSONEncoder): pass elif hasattr(obj, '__iter__'): return tuple(item for item in obj) - elif (coreapi is not None) and isinstance(obj, (coreapi.Document, coreapi.Error)): - raise RuntimeError( - 'Cannot return a coreapi object from a JSON view. ' - 'You should be using a schema renderer instead for this view.' - ) return super(JSONEncoder, self).default(obj) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 7687448c4..05434b72e 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -98,7 +98,6 @@ class ViewSetMixin(object): # resolved URL. view.cls = cls view.suffix = initkwargs.get('suffix', None) - view.actions = actions return csrf_exempt(view) def initialize_request(self, request, *args, **kwargs): diff --git a/runtests.py b/runtests.py index e97ac0367..1627e33b2 100755 --- a/runtests.py +++ b/runtests.py @@ -14,7 +14,7 @@ PYTEST_ARGS = { FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501'] -ISORT_ARGS = ['--recursive', '--check-only', '-o' 'uritemplate', '-p', 'tests', 'rest_framework', 'tests'] +ISORT_ARGS = ['--recursive', '--check-only', '-p', 'tests', 'rest_framework', 'tests'] sys.path.append(os.path.dirname(__file__)) diff --git a/schema-support b/schema-support deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_routers.py b/tests/test_routers.py index f45039f80..acab660d8 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -257,7 +257,7 @@ class TestNameableRoot(TestCase): def test_router_has_custom_name(self): expected = 'nameable-root' - self.assertEqual(expected, self.urls[-1].name) + self.assertEqual(expected, self.urls[0].name) class TestActionKeywordArgs(TestCase): diff --git a/tests/test_schemas.py b/tests/test_schemas.py deleted file mode 100644 index 7d3308ed9..000000000 --- a/tests/test_schemas.py +++ /dev/null @@ -1,137 +0,0 @@ -import unittest - -from django.conf.urls import include, url -from django.test import TestCase, override_settings - -from rest_framework import filters, pagination, permissions, serializers -from rest_framework.compat import coreapi -from rest_framework.routers import DefaultRouter -from rest_framework.test import APIClient -from rest_framework.viewsets import ModelViewSet - - -class MockUser(object): - def is_authenticated(self): - return True - - -class ExamplePagination(pagination.PageNumberPagination): - page_size = 100 - - -class ExampleSerializer(serializers.Serializer): - a = serializers.CharField(required=True) - b = serializers.CharField(required=False) - - -class ExampleViewSet(ModelViewSet): - pagination_class = ExamplePagination - permission_classes = [permissions.IsAuthenticatedOrReadOnly] - filter_backends = [filters.OrderingFilter] - serializer_class = ExampleSerializer - - -router = DefaultRouter(schema_title='Example API' if coreapi else None) -router.register('example', ExampleViewSet, base_name='example') -urlpatterns = [ - url(r'^', include(router.urls)) -] - - -@unittest.skipUnless(coreapi, 'coreapi is not installed') -@override_settings(ROOT_URLCONF='tests.test_schemas') -class TestRouterGeneratedSchema(TestCase): - def test_anonymous_request(self): - client = APIClient() - response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json') - self.assertEqual(response.status_code, 200) - expected = coreapi.Document( - url='', - title='Example API', - content={ - 'example': { - 'list': coreapi.Link( - url='/example/', - action='get', - fields=[ - coreapi.Field('page', required=False, location='query'), - coreapi.Field('ordering', required=False, location='query') - ] - ), - 'retrieve': coreapi.Link( - url='/example/{pk}/', - action='get', - fields=[ - coreapi.Field('pk', required=True, location='path') - ] - ) - } - } - ) - self.assertEqual(response.data, expected) - - def test_authenticated_request(self): - client = APIClient() - client.force_authenticate(MockUser()) - response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json') - self.assertEqual(response.status_code, 200) - expected = coreapi.Document( - url='', - title='Example API', - content={ - 'example': { - 'list': coreapi.Link( - url='/example/', - action='get', - fields=[ - coreapi.Field('page', required=False, location='query'), - coreapi.Field('ordering', required=False, location='query') - ] - ), - 'create': coreapi.Link( - url='/example/', - action='post', - encoding='application/json', - fields=[ - coreapi.Field('a', required=True, location='form'), - coreapi.Field('b', required=False, location='form') - ] - ), - 'retrieve': coreapi.Link( - url='/example/{pk}/', - action='get', - fields=[ - coreapi.Field('pk', required=True, location='path') - ] - ), - 'update': coreapi.Link( - url='/example/{pk}/', - action='put', - encoding='application/json', - fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=True, location='form'), - coreapi.Field('b', required=False, location='form') - ] - ), - 'partial_update': coreapi.Link( - url='/example/{pk}/', - action='patch', - encoding='application/json', - fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=False, location='form'), - coreapi.Field('b', required=False, location='form') - ] - ), - 'destroy': coreapi.Link( - url='/example/{pk}/', - action='delete', - fields=[ - coreapi.Field('pk', required=True, location='path') - ] - ) - } - } - ) - self.assertEqual(response.data, expected)