From 43f78b784dbe65794bfa3ad7055fbe642c374b48 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Wed, 15 Jan 2025 01:12:10 -0500 Subject: [PATCH 1/5] Update examples to use inline dependencies; remove redundant examples --- docs/examples.rst | 172 +++---------------------- examples/flask_example.py | 23 +++- examples/inflection_example.py | 8 ++ examples/invalid_package.json | 7 ++ examples/package.json | 13 ++ examples/package_json_example.py | 7 ++ examples/peewee_example.py | 210 ------------------------------- examples/textblob_example.py | 33 ----- 8 files changed, 75 insertions(+), 398 deletions(-) create mode 100644 examples/invalid_package.json create mode 100644 examples/package.json delete mode 100644 examples/peewee_example.py delete mode 100644 examples/textblob_example.py diff --git a/docs/examples.rst b/docs/examples.rst index d617d9a78..e14eb3f06 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -2,6 +2,11 @@ Examples ******** +The below examples demonstrate how to use marshmallow in various contexts. +To run each example, you will need to have [uv](https://docs.astral.sh/uv/getting-started/installation/) installed. +The examples use the [PEP 723 inline metadata](https://peps.python.org/pep-0723/) +to declare the dependencies of each script. ``uv`` will install the depedencies automatically when running a script. + Validating ``package.json`` =========================== @@ -21,28 +26,15 @@ Below is a schema that could be used to validate Given the following ``package.json`` file... -.. code-block:: json - - { - "name": "dunderscore", - "version": "1.2.3", - "description": "The Pythonic JavaScript toolkit", - "devDependencies": { - "pest": "^23.4.1" - }, - "main": "index.js", - "scripts": { - "test": "pest" - }, - "license": "MIT" - } +.. literalinclude:: ../examples/package.json + :language: json We can validate it using the above script. .. code-block:: shell-session - $ python examples/package_json_example.py < package.json + $ uv run examples/package_json_example.py < examples/package.json {'description': 'The Pythonic JavaScript toolkit', 'dev_dependencies': {'pest': '^23.4.1'}, 'license': 'MIT', @@ -55,82 +47,17 @@ Notice that our custom field deserialized the version string to a ``Version`` ob But if we pass an invalid package.json file... -.. code-block:: json - - { - "name": "dunderscore", - "version": "INVALID", - "homepage": "INVALID", - "description": "The Pythonic JavaScript toolkit", - "license": "MIT" - } +.. literalinclude:: ../examples/invalid_package.json + :language: json We see the corresponding error messages. .. code-block:: shell-session - $ python examples/package_json_example.py < invalid_package.json + $ uv run examples/package_json_example.py < examples/invalid_package.json ERROR: package.json is invalid {'homepage': ['Not a valid URL.'], 'version': ['Not a valid version.']} - - -Text Analysis API (Bottle + TextBlob) -===================================== - -Here is a very simple text analysis API using `Bottle `_ and `TextBlob `_ that demonstrates how to declare an object serializer. - -Assume that ``TextBlob`` objects have ``polarity``, ``subjectivity``, ``noun_phrase``, ``tags``, and ``words`` properties. - -.. literalinclude:: ../examples/textblob_example.py - :language: python - -**Using The API** - -First, run the app. - -.. code-block:: shell-session - - $ python examples/textblob_example.py - -Then send a POST request with some text with `httpie `_ (a curl-like tool) for testing the APIs. - - -.. code-block:: shell-session - - $ pip install httpie - $ http POST :5000/api/v1/analyze text="Simple is better" - HTTP/1.0 200 OK - Content-Length: 189 - Content-Type: application/json - Date: Wed, 13 Nov 2013 08:58:40 GMT - Server: WSGIServer/0.1 Python/2.7.5 - - { - "chunks": [ - "simple" - ], - "discrete_sentiment": "positive", - "polarity": 0.25, - "subjectivity": 0.4285714285714286, - "tags": [ - [ - "Simple", - "NN" - ], - [ - "is", - "VBZ" - ], - [ - "better", - "JJR" - ] - ], - "word_count": 3 - } - - Quotes API (Flask + SQLAlchemy) =============================== @@ -152,14 +79,19 @@ Run the app. .. code-block:: shell-session - $ pip install flask flask-sqlalchemy - $ python examples/flask_example.py + $ uv run examples/flask_example.py + +We'll use the [httpie cli](https://httpie.io/cli) to send requests +Install it with ``uv``. + +.. code-block:: shell-session + + $ uv tool install httpie First we'll POST some quotes. .. code-block:: shell-session - $ pip install httpie $ http POST :5000/quotes/ author="Tim Peters" content="Beautiful is better than ugly." $ http POST :5000/quotes/ author="Tim Peters" content="Now is better than never." $ http POST :5000/quotes/ author="Peter Hintjens" content="Simplicity is always better than functionality." @@ -222,72 +154,6 @@ We can also GET the quotes for a single author. ] } -ToDo API (Flask + Peewee) -========================= - -This example uses Flask and the `Peewee `_ ORM to create a basic Todo application. - -Here, we use `Schema.load ` to validate and deserialize input data to model data. Also notice how `pre_load ` is used to clean input data and `post_load ` is used to add an envelope to response data. - -.. literalinclude:: ../examples/peewee_example.py - :language: python - -**Using the API** - -Run the app. - -.. code-block:: shell-session - - $ pip install flask peewee - $ python examples/peewee_example.py - -After registering a user and creating some todo items in the database, here is an example response. - -.. code-block:: shell-session - - $ pip install httpie - $ http GET :5000/todos/ - { - "todos": [ - { - "content": "Install marshmallow", - "done": false, - "id": 1, - "posted_on": "2015-05-05T01:51:12.832232+00:00", - "user": { - "user": { - "email": "foo@bar.com", - "id": 1 - } - } - }, - { - "content": "Learn Python", - "done": false, - "id": 2, - "posted_on": "2015-05-05T01:51:20.728052+00:00", - "user": { - "user": { - "email": "foo@bar.com", - "id": 1 - } - } - }, - { - "content": "Refactor everything", - "done": false, - "id": 3, - "posted_on": "2015-05-05T01:51:25.970153+00:00", - "user": { - "user": { - "email": "foo@bar.com", - "id": 1 - } - } - } - ] - } - Inflection (camel-cased keys) ============================= diff --git a/examples/flask_example.py b/examples/flask_example.py index 69a1520df..f216838c4 100644 --- a/examples/flask_example.py +++ b/examples/flask_example.py @@ -1,14 +1,32 @@ +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "flask", +# "flask-sqlalchemy>=3.1.1", +# "marshmallow", +# "sqlalchemy>2.0", +# ] +# /// +from __future__ import annotations + import datetime from flask import Flask, request from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import NoResultFound +from sqlalchemy.orm import DeclarativeBase from marshmallow import Schema, ValidationError, fields, pre_load app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/quotes.db" -db = SQLAlchemy(app) + + +class Base(DeclarativeBase): + pass + + +db = SQLAlchemy(app, model_class=Base) ##### MODELS ##### @@ -139,5 +157,6 @@ def new_quote(): if __name__ == "__main__": - db.create_all() + with app.app_context(): + db.create_all() app.run(debug=True, port=5000) diff --git a/examples/inflection_example.py b/examples/inflection_example.py index 72eeb35b9..bc8560c1b 100644 --- a/examples/inflection_example.py +++ b/examples/inflection_example.py @@ -1,3 +1,9 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "marshmallow", +# ] +# /// from marshmallow import Schema, fields @@ -25,6 +31,8 @@ class UserSchema(CamelCaseSchema): schema = UserSchema() loaded = schema.load({"firstName": "David", "lastName": "Bowie"}) +print("Loaded data:") print(loaded) # => {'last_name': 'Bowie', 'first_name': 'David'} dumped = schema.dump(loaded) +print("Dumped data:") print(dumped) # => {'lastName': 'Bowie', 'firstName': 'David'} diff --git a/examples/invalid_package.json b/examples/invalid_package.json new file mode 100644 index 000000000..a2c4af80b --- /dev/null +++ b/examples/invalid_package.json @@ -0,0 +1,7 @@ +{ + "name": "dunderscore", + "version": "INVALID", + "homepage": "INVALID", + "description": "The Pythonic JavaScript toolkit", + "license": "MIT" +} diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 000000000..ee40263f4 --- /dev/null +++ b/examples/package.json @@ -0,0 +1,13 @@ +{ + "name": "dunderscore", + "version": "1.2.3", + "description": "The Pythonic JavaScript toolkit", + "devDependencies": { + "pest": "^23.4.1" + }, + "main": "index.js", + "scripts": { + "test": "pest" + }, + "license": "MIT" +} diff --git a/examples/package_json_example.py b/examples/package_json_example.py index 03a3d3b7c..e32023000 100644 --- a/examples/package_json_example.py +++ b/examples/package_json_example.py @@ -1,3 +1,10 @@ +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "marshmallow", +# "packaging>=17.0", +# ] +# /// import json import sys from pprint import pprint diff --git a/examples/peewee_example.py b/examples/peewee_example.py deleted file mode 100644 index 0e99e5dbf..000000000 --- a/examples/peewee_example.py +++ /dev/null @@ -1,210 +0,0 @@ -import datetime as dt -from functools import wraps - -import peewee as pw -from flask import Flask, g, jsonify, request - -from marshmallow import ( - Schema, - ValidationError, - fields, - post_dump, - post_load, - pre_load, - validate, -) - -app = Flask(__name__) -db = pw.SqliteDatabase("/tmp/todo.db") - -###### MODELS ##### - - -class BaseModel(pw.Model): - """Base model class. All descendants share the same database.""" - - class Meta: - database = db - - -class User(BaseModel): - email = pw.CharField(max_length=80, unique=True) - password = pw.CharField() - joined_on = pw.DateTimeField() - - -class Todo(BaseModel): - content = pw.TextField() - is_done = pw.BooleanField(default=False) - user = pw.ForeignKeyField(User) - posted_on = pw.DateTimeField() - - -def create_tables(): - db.connect() - User.create_table(True) - Todo.create_table(True) - - -##### SCHEMAS ##### - - -class UserSchema(Schema): - id = fields.Int(dump_only=True) - email = fields.Str( - required=True, validate=validate.Email(error="Not a valid email address") - ) - password = fields.Str( - required=True, validate=[validate.Length(min=6, max=36)], load_only=True - ) - joined_on = fields.DateTime(dump_only=True) - - # Clean up data - @pre_load - def process_input(self, data, **kwargs): - data["email"] = data["email"].lower().strip() - return data - - # We add a post_dump hook to add an envelope to responses - @post_dump(pass_many=True) - def wrap(self, data, many, **kwargs): - key = "users" if many else "user" - return {key: data} - - -class TodoSchema(Schema): - id = fields.Int(dump_only=True) - done = fields.Boolean(attribute="is_done", missing=False) - user = fields.Nested(UserSchema(exclude=("joined_on", "password")), dump_only=True) - content = fields.Str(required=True) - posted_on = fields.DateTime(dump_only=True) - - # Again, add an envelope to responses - @post_dump(pass_many=True) - def wrap(self, data, many, **kwargs): - key = "todos" if many else "todo" - return {key: data} - - # We use make_object to create a new Todo from validated data - @post_load - def make_object(self, data, **kwargs): - if not data: - return None - return Todo( - content=data["content"], - is_done=data["is_done"], - posted_on=dt.datetime.now(dt.timezone.utc), - ) - - -user_schema = UserSchema() -todo_schema = TodoSchema() -todos_schema = TodoSchema(many=True) - -###### HELPERS ###### - - -def check_auth(email, password): - """Check if a username/password combination is valid.""" - try: - user = User.get(User.email == email) - except User.DoesNotExist: - return False - return password == user.password - - -def requires_auth(f): - @wraps(f) - def decorated(*args, **kwargs): - auth = request.authorization - if not auth or not check_auth(auth.username, auth.password): - resp = jsonify({"message": "Please authenticate."}) - resp.status_code = 401 - resp.headers["WWW-Authenticate"] = 'Basic realm="Example"' - return resp - kwargs["user"] = User.get(User.email == auth.username) - return f(*args, **kwargs) - - return decorated - - -# Ensure a separate connection for each thread -@app.before_request -def before_request(): - g.db = db - g.db.connect() - - -@app.after_request -def after_request(response): - g.db.close() - return response - - -#### API ##### - - -@app.route("/register", methods=["POST"]) -def register(): - json_input = request.get_json() - try: - data = user_schema.load(json_input) - except ValidationError as err: - return {"errors": err.messages}, 422 - try: # Use get to see if user already exists - User.get(User.email == data["email"]) - except User.DoesNotExist: - user = User.create( - email=data["email"], joined_on=dt.datetime.now(), password=data["password"] - ) - message = f"Successfully created user: {user.email}" - else: - return {"errors": "That email address is already in the database"}, 400 - - data = user_schema.dump(user) - data["message"] = message - return data, 201 - - -@app.route("/todos/", methods=["GET"]) -def get_todos(): - todos = Todo.select().order_by(Todo.posted_on.asc()) # Get all todos - return todos_schema.dump(list(todos)) - - -@app.route("/todos/") -def get_todo(pk): - todo = Todo.get(Todo.id == pk) - if not todo: - return {"errors": "Todo could not be find"}, 404 - return todo_schema.dump(todo) - - -@app.route("/todos//toggle", methods=["POST", "PUT"]) -def toggledone(pk): - try: - todo = Todo.get(Todo.id == pk) - except Todo.DoesNotExist: - return {"message": "Todo could not be found"}, 404 - status = not todo.is_done - update_query = todo.update(is_done=status) - update_query.execute() - return todo_schema.dump(todo) - - -@app.route("/todos/", methods=["POST"]) -@requires_auth -def new_todo(user): - json_input = request.get_json() - try: - todo = todo_schema.load(json_input) - except ValidationError as err: - return {"errors": err.messages}, 422 - todo.user = user - todo.save() - return todo_schema.dump(todo) - - -if __name__ == "__main__": - create_tables() - app.run(port=5000, debug=True) diff --git a/examples/textblob_example.py b/examples/textblob_example.py deleted file mode 100644 index b5da2c238..000000000 --- a/examples/textblob_example.py +++ /dev/null @@ -1,33 +0,0 @@ -from bottle import request, route, run -from textblob import TextBlob - -from marshmallow import Schema, fields - - -class BlobSchema(Schema): - polarity = fields.Float() - subjectivity = fields.Float() - chunks = fields.List(fields.String, attribute="noun_phrases") - tags = fields.Raw() - discrete_sentiment = fields.Method("get_discrete_sentiment") - word_count = fields.Function(lambda obj: len(obj.words)) - - def get_discrete_sentiment(self, obj): - if obj.polarity > 0.1: - return "positive" - elif obj.polarity < -0.1: - return "negative" - else: - return "neutral" - - -blob_schema = BlobSchema() - - -@route("/api/v1/analyze", method="POST") -def analyze(): - blob = TextBlob(request.json["text"]) - return blob_schema.dump(blob) - - -run(reloader=True, port=5000) From 8071eb94cff4405f9cb56c824440355e38ba010a Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Wed, 15 Jan 2025 01:13:12 -0500 Subject: [PATCH 2/5] Update changelog --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2d0651e33..b0fd8e195 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,7 +12,7 @@ Features: Documentation: -- Various documentation improvements (:pr:`2757`, :pr:`2759`, :pr:`2765`, :pr:`2774`). +- Various documentation improvements (:pr:`2757`, :pr:`2759`, :pr:`2765`, :pr:`2774`, :pr:`2778`). Deprecations: From c832dbefac7d3e0bf2a1cc262b912148f54520d8 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Wed, 15 Jan 2025 01:29:18 -0500 Subject: [PATCH 3/5] Use SQLA 2 API --- docs/examples.rst | 7 ++++--- examples/flask_example.py | 22 +++++++++++----------- examples/inflection_example.py | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index e14eb3f06..1e4df12b3 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -3,9 +3,10 @@ Examples ******** The below examples demonstrate how to use marshmallow in various contexts. -To run each example, you will need to have [uv](https://docs.astral.sh/uv/getting-started/installation/) installed. -The examples use the [PEP 723 inline metadata](https://peps.python.org/pep-0723/) -to declare the dependencies of each script. ``uv`` will install the depedencies automatically when running a script. +To run each example, you will need to have `uv `_ installed. +The examples use `PEP 723 inline metadata `_ +to declare the dependencies of each script. ``uv`` will install the +dependencies automatically when running these scripts. Validating ``package.json`` =========================== diff --git a/examples/flask_example.py b/examples/flask_example.py index f216838c4..12cdb2c09 100644 --- a/examples/flask_example.py +++ b/examples/flask_example.py @@ -14,7 +14,7 @@ from flask import Flask, request from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import NoResultFound -from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship from marshmallow import Schema, ValidationError, fields, pre_load @@ -31,18 +31,18 @@ class Base(DeclarativeBase): ##### MODELS ##### -class Author(db.Model): # type: ignore - id = db.Column(db.Integer, primary_key=True) - first = db.Column(db.String(80)) - last = db.Column(db.String(80)) +class Author(db.Model): # type: ignore[name-defined] + id: Mapped[int] = mapped_column(primary_key=True) + first: Mapped[str] + last: Mapped[str] -class Quote(db.Model): # type: ignore - id = db.Column(db.Integer, primary_key=True) - content = db.Column(db.String, nullable=False) - author_id = db.Column(db.Integer, db.ForeignKey("author.id")) - author = db.relationship("Author", backref=db.backref("quotes", lazy="dynamic")) - posted_at = db.Column(db.DateTime) +class Quote(db.Model): # type: ignore[name-defined] + id: Mapped[int] = mapped_column(primary_key=True) + content: Mapped[str] = mapped_column(nullable=False) + author_id: Mapped[int] = mapped_column(db.ForeignKey(Author.id)) + author: Mapped[Author] = relationship(backref=db.backref("quotes", lazy="dynamic")) # type: ignore[assignment] + posted_at: Mapped[datetime.datetime] ##### SCHEMAS ##### diff --git a/examples/inflection_example.py b/examples/inflection_example.py index bc8560c1b..2a8e65fe7 100644 --- a/examples/inflection_example.py +++ b/examples/inflection_example.py @@ -1,5 +1,5 @@ # /// script -# requires-python = ">=3.13" +# requires-python = ">=3.9" # dependencies = [ # "marshmallow", # ] From 8043b091f45c666d180318f9c9fbda6fe3089712 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Wed, 15 Jan 2025 10:14:42 -0500 Subject: [PATCH 4/5] Fix link, add run instructions --- docs/examples.rst | 12 +++++++++++- examples/inflection_example.py | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 1e4df12b3..9920f53e6 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -82,7 +82,7 @@ Run the app. $ uv run examples/flask_example.py -We'll use the [httpie cli](https://httpie.io/cli) to send requests +We'll use the `httpie cli `_ to send requests Install it with ``uv``. .. code-block:: shell-session @@ -164,3 +164,13 @@ HTTP APIs will often use camel-cased keys for their input and output representat .. literalinclude:: ../examples/inflection_example.py :language: python + +To run the example: + +.. code-block:: shell-session + + $ uv run examples/inflection_example.py + Loaded data: + {'first_name': 'David', 'last_name': 'Bowie'} + Dumped data: + {'firstName': 'David', 'lastName': 'Bowie'} diff --git a/examples/inflection_example.py b/examples/inflection_example.py index 2a8e65fe7..43e08a1c5 100644 --- a/examples/inflection_example.py +++ b/examples/inflection_example.py @@ -32,7 +32,7 @@ class UserSchema(CamelCaseSchema): schema = UserSchema() loaded = schema.load({"firstName": "David", "lastName": "Bowie"}) print("Loaded data:") -print(loaded) # => {'last_name': 'Bowie', 'first_name': 'David'} +print(loaded) dumped = schema.dump(loaded) print("Dumped data:") -print(dumped) # => {'lastName': 'Bowie', 'firstName': 'David'} +print(dumped) From 30017fa9879ab5eda4feae530ebc57a98d6edf82 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Wed, 15 Jan 2025 11:56:12 -0500 Subject: [PATCH 5/5] Split examples into separate files --- docs/examples.rst | 176 ---------------------- docs/examples/index.rst | 16 ++ docs/examples/inflection.rst | 19 +++ docs/examples/quotes_api.rst | 96 ++++++++++++ docs/examples/validating_package_json.rst | 51 +++++++ docs/index.rst | 4 +- 6 files changed, 184 insertions(+), 178 deletions(-) delete mode 100644 docs/examples.rst create mode 100644 docs/examples/index.rst create mode 100644 docs/examples/inflection.rst create mode 100644 docs/examples/quotes_api.rst create mode 100644 docs/examples/validating_package_json.rst diff --git a/docs/examples.rst b/docs/examples.rst deleted file mode 100644 index 9920f53e6..000000000 --- a/docs/examples.rst +++ /dev/null @@ -1,176 +0,0 @@ -******** -Examples -******** - -The below examples demonstrate how to use marshmallow in various contexts. -To run each example, you will need to have `uv `_ installed. -The examples use `PEP 723 inline metadata `_ -to declare the dependencies of each script. ``uv`` will install the -dependencies automatically when running these scripts. - -Validating ``package.json`` -=========================== - -marshmallow can be used to validate configuration according to a schema. -Below is a schema that could be used to validate -``package.json`` files. This example demonstrates the following features: - - -- Validation and deserialization using `Schema.load ` -- :doc:`Custom fields ` -- Specifying deserialization keys using ``data_key`` -- Including unknown keys using ``unknown = INCLUDE`` - -.. literalinclude:: ../examples/package_json_example.py - :language: python - - -Given the following ``package.json`` file... - -.. literalinclude:: ../examples/package.json - :language: json - - -We can validate it using the above script. - -.. code-block:: shell-session - - $ uv run examples/package_json_example.py < examples/package.json - {'description': 'The Pythonic JavaScript toolkit', - 'dev_dependencies': {'pest': '^23.4.1'}, - 'license': 'MIT', - 'main': 'index.js', - 'name': 'dunderscore', - 'scripts': {'test': 'pest'}, - 'version': } - -Notice that our custom field deserialized the version string to a ``Version`` object. - -But if we pass an invalid package.json file... - -.. literalinclude:: ../examples/invalid_package.json - :language: json - -We see the corresponding error messages. - -.. code-block:: shell-session - - $ uv run examples/package_json_example.py < examples/invalid_package.json - ERROR: package.json is invalid - {'homepage': ['Not a valid URL.'], 'version': ['Not a valid version.']} - -Quotes API (Flask + SQLAlchemy) -=============================== - -Below is a full example of a REST API for a quotes app using `Flask `_ and `SQLAlchemy `_ with marshmallow. It demonstrates a number of features, including: - -- Custom validation -- Nesting fields -- Using ``dump_only=True`` to specify read-only fields -- Output filtering using the ``only`` parameter -- Using `@pre_load ` to preprocess input data. - -.. literalinclude:: ../examples/flask_example.py - :language: python - - -**Using The API** - -Run the app. - -.. code-block:: shell-session - - $ uv run examples/flask_example.py - -We'll use the `httpie cli `_ to send requests -Install it with ``uv``. - -.. code-block:: shell-session - - $ uv tool install httpie - -First we'll POST some quotes. - -.. code-block:: shell-session - - $ http POST :5000/quotes/ author="Tim Peters" content="Beautiful is better than ugly." - $ http POST :5000/quotes/ author="Tim Peters" content="Now is better than never." - $ http POST :5000/quotes/ author="Peter Hintjens" content="Simplicity is always better than functionality." - - -If we provide invalid input data, we get 400 error response. Let's omit "author" from the input data. - -.. code-block:: shell-session - - $ http POST :5000/quotes/ content="I have no author" - { - "author": [ - "Data not provided." - ] - } - -Now we can GET a list of all the quotes. - -.. code-block:: shell-session - - $ http :5000/quotes/ - { - "quotes": [ - { - "content": "Beautiful is better than ugly.", - "id": 1 - }, - { - "content": "Now is better than never.", - "id": 2 - }, - { - "content": "Simplicity is always better than functionality.", - "id": 3 - } - ] - } - -We can also GET the quotes for a single author. - -.. code-block:: shell-session - - $ http :5000/authors/1 - { - "author": { - "first": "Tim", - "formatted_name": "Peters, Tim", - "id": 1, - "last": "Peters" - }, - "quotes": [ - { - "content": "Beautiful is better than ugly.", - "id": 1 - }, - { - "content": "Now is better than never.", - "id": 2 - } - ] - } - - -Inflection (camel-cased keys) -============================= - -HTTP APIs will often use camel-cased keys for their input and output representations. This example shows how you can use the -`Schema.on_bind_field ` hook to automatically inflect keys. - -.. literalinclude:: ../examples/inflection_example.py - :language: python - -To run the example: - -.. code-block:: shell-session - - $ uv run examples/inflection_example.py - Loaded data: - {'first_name': 'David', 'last_name': 'Bowie'} - Dumped data: - {'firstName': 'David', 'lastName': 'Bowie'} diff --git a/docs/examples/index.rst b/docs/examples/index.rst new file mode 100644 index 000000000..bdcd13281 --- /dev/null +++ b/docs/examples/index.rst @@ -0,0 +1,16 @@ +******** +Examples +******** + +The below examples demonstrate how to use marshmallow in various contexts. +To run each example, you will need to have `uv `_ installed. +The examples use `PEP 723 inline metadata `_ +to declare the dependencies of each script. ``uv`` will install the +dependencies automatically when running these scripts. + +.. toctree:: + :maxdepth: 1 + + validating_package_json + quotes_api + inflection diff --git a/docs/examples/inflection.rst b/docs/examples/inflection.rst new file mode 100644 index 000000000..5bc0bff48 --- /dev/null +++ b/docs/examples/inflection.rst @@ -0,0 +1,19 @@ +***************************** +Inflection (camel-cased keys) +***************************** + +HTTP APIs will often use camel-cased keys for their input and output representations. This example shows how you can use the +`Schema.on_bind_field ` hook to automatically inflect keys. + +.. literalinclude:: ../../examples/inflection_example.py + :language: python + +To run the example: + +.. code-block:: shell-session + + $ uv run examples/inflection_example.py + Loaded data: + {'first_name': 'David', 'last_name': 'Bowie'} + Dumped data: + {'firstName': 'David', 'lastName': 'Bowie'} diff --git a/docs/examples/quotes_api.rst b/docs/examples/quotes_api.rst new file mode 100644 index 000000000..a0c6c2bc2 --- /dev/null +++ b/docs/examples/quotes_api.rst @@ -0,0 +1,96 @@ +******************************* +Quotes API (Flask + SQLAlchemy) +******************************* + +Below is a full example of a REST API for a quotes app using `Flask `_ and `SQLAlchemy `_ with marshmallow. It demonstrates a number of features, including: + +- Custom validation +- Nesting fields +- Using ``dump_only=True`` to specify read-only fields +- Output filtering using the ``only`` parameter +- Using `@pre_load ` to preprocess input data. + +.. literalinclude:: ../../examples/flask_example.py + :language: python + + +**Using The API** + +Run the app. + +.. code-block:: shell-session + + $ uv run examples/flask_example.py + +We'll use the `httpie cli `_ to send requests +Install it with ``uv``. + +.. code-block:: shell-session + + $ uv tool install httpie + +First we'll POST some quotes. + +.. code-block:: shell-session + + $ http POST :5000/quotes/ author="Tim Peters" content="Beautiful is better than ugly." + $ http POST :5000/quotes/ author="Tim Peters" content="Now is better than never." + $ http POST :5000/quotes/ author="Peter Hintjens" content="Simplicity is always better than functionality." + + +If we provide invalid input data, we get 400 error response. Let's omit "author" from the input data. + +.. code-block:: shell-session + + $ http POST :5000/quotes/ content="I have no author" + { + "author": [ + "Data not provided." + ] + } + +Now we can GET a list of all the quotes. + +.. code-block:: shell-session + + $ http :5000/quotes/ + { + "quotes": [ + { + "content": "Beautiful is better than ugly.", + "id": 1 + }, + { + "content": "Now is better than never.", + "id": 2 + }, + { + "content": "Simplicity is always better than functionality.", + "id": 3 + } + ] + } + +We can also GET the quotes for a single author. + +.. code-block:: shell-session + + $ http :5000/authors/1 + { + "author": { + "first": "Tim", + "formatted_name": "Peters, Tim", + "id": 1, + "last": "Peters" + }, + "quotes": [ + { + "content": "Beautiful is better than ugly.", + "id": 1 + }, + { + "content": "Now is better than never.", + "id": 2 + } + ] + } diff --git a/docs/examples/validating_package_json.rst b/docs/examples/validating_package_json.rst new file mode 100644 index 000000000..177688c6a --- /dev/null +++ b/docs/examples/validating_package_json.rst @@ -0,0 +1,51 @@ +*************************** +Validating ``package.json`` +*************************** + +marshmallow can be used to validate configuration according to a schema. +Below is a schema that could be used to validate +``package.json`` files. This example demonstrates the following features: + + +- Validation and deserialization using `Schema.load ` +- :doc:`Custom fields ` +- Specifying deserialization keys using ``data_key`` +- Including unknown keys using ``unknown = INCLUDE`` + +.. literalinclude:: ../../examples/package_json_example.py + :language: python + + +Given the following ``package.json`` file... + +.. literalinclude:: ../../examples/package.json + :language: json + + +We can validate it using the above script. + +.. code-block:: shell-session + + $ uv run examples/package_json_example.py < examples/package.json + {'description': 'The Pythonic JavaScript toolkit', + 'dev_dependencies': {'pest': '^23.4.1'}, + 'license': 'MIT', + 'main': 'index.js', + 'name': 'dunderscore', + 'scripts': {'test': 'pest'}, + 'version': } + +Notice that our custom field deserialized the version string to a ``Version`` object. + +But if we pass an invalid package.json file... + +.. literalinclude:: ../../examples/invalid_package.json + :language: json + +We see the corresponding error messages. + +.. code-block:: shell-session + + $ uv run examples/package_json_example.py < examples/invalid_package.json + ERROR: package.json is invalid + {'homepage': ['Not a valid URL.'], 'version': ['Not a valid version.']} diff --git a/docs/index.rst b/docs/index.rst index 30595ed91..b2dded667 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,7 +16,7 @@ Release v\ |version|. (:doc:`changelog`) :start-after: .. start elevator-pitch :end-before: .. end elevator-pitch -Ready to get started? Go on to the :doc:`quickstart` or check out some :doc:`examples `. +Ready to get started? Go on to the :doc:`quickstart` or check out some :doc:`examples `. Upgrading from an older version? ================================ @@ -54,7 +54,7 @@ Usage guide nesting custom_fields extending - examples + examples/index API reference