Skip to content

Commit 25a5652

Browse files
authored
Merge pull request #462 from realpython/python-serialize
Python Serialize: Materials
2 parents 7698bf7 + 087fbd0 commit 25a5652

File tree

56 files changed

+1634
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1634
-0
lines changed

python-serialize/README.md

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
# Serialize Your Data With Python
2+
3+
This folder contains the sample code for the tutorial [Serialize Your Data With Python](https://realpython.com/python-serialize-data/) published on Real Python.
4+
5+
## Table of Contents
6+
7+
- [Setup](#setup)
8+
- [Usage](#usage)
9+
- [Python Objects](##python-objects)
10+
- [Standard Python](#standard-python)
11+
- [Customize Pickle](#customize-pickle)
12+
- [JSON Encode](#json-encode)
13+
- [Foreign Formats](#foreign-formats)
14+
- [Executable Code](#executable-code)
15+
- [Pickle-Importable Code](#pickle-importable-code)
16+
- [Code Objects](#code-objects)
17+
- [Digital Signature](#digital-signature)
18+
- [HTTP Payload](#http-payload)
19+
- [Flask](#flask)
20+
- [Django REST Framework](#django-rest-framework)
21+
- [FastAPI](#fastapi)
22+
- [Pydantic](#pydantic)
23+
- [Hierarchical Data](#hierarchical-data)
24+
- [XML, YAML, JSON, BSON](#xml-yaml-json-bson)
25+
- [Tabular Data](#tabular-data)
26+
- [CSV](#csv)
27+
- [Apache Parquet](#apache-parquet)
28+
- [Schema-Based Formats](#schema-based-formats)
29+
- [Apache Avro](#apache-avro)
30+
- [Protocol Buffers (Protobuf)](#protocol-buffers-protobuf)
31+
32+
## Setup
33+
34+
Create and activate a new virtual environment:
35+
36+
```shell
37+
$ python3 -m venv venv/
38+
$ source venv/bin/activate
39+
```
40+
41+
Install the required third-party dependencies:
42+
43+
```shell
44+
(venv) $ python -m pip install -r requirements.txt
45+
```
46+
47+
## Usage
48+
49+
### Python Objects
50+
51+
#### Standard Python
52+
53+
```shell
54+
(venv) $ cd python-objects/standard-python/
55+
(venv) $ python pickle_demo.py
56+
(venv) $ python marshal_demo.py
57+
(venv) $ python shelve_demo.py
58+
(venv) $ python dbm_demo.py
59+
```
60+
61+
#### Customize Pickle
62+
63+
```shell
64+
(venv) $ cd python-objects/customize-pickle/
65+
(venv) $ python main.py
66+
```
67+
68+
#### JSON Encode
69+
70+
```shell
71+
(venv) $ cd python-objects/json-encode/
72+
(venv) $ python main.py
73+
```
74+
75+
#### Foreign Formats
76+
77+
jsonpickle and PyYAML:
78+
79+
```shell
80+
(venv) $ cd python-objects/foreign-formats/
81+
(venv) $ python jsonpickle_demo.py
82+
(venv) $ python pyyaml_demo.py
83+
```
84+
85+
### Executable Code
86+
87+
#### Pickle-Importable Code
88+
89+
```shell
90+
(venv) $ cd executable-code/pickle-importable/
91+
(venv) $ python main.py
92+
```
93+
94+
#### Code Objects
95+
96+
```shell
97+
(venv) $ cd executable-code/code-objects/
98+
(venv) $ python dill_demo.py
99+
```
100+
101+
#### Digital Signature
102+
103+
```shell
104+
(venv) $ cd executable-code/digital-signature/
105+
(venv) $ python main.py
106+
```
107+
108+
### HTTP Payload
109+
110+
#### Flask
111+
112+
Start the web server:
113+
114+
```shell
115+
(venv) $ cd http-payload/flask-rest-api/
116+
(venv) $ flask --app main --debug run
117+
```
118+
119+
Navigate to the "users" resource in your web browser:
120+
<http://127.0.0.1:5000/users>
121+
122+
Send an HTTP GET request to retrieve all users:
123+
124+
```shell
125+
$ curl -s http://127.0.0.1:5000/users | jq
126+
[
127+
{
128+
"name": "Alice",
129+
"id": "512a956f-165a-429f-9ec8-83d859843072",
130+
"created_at": "2023-11-13T12:29:18.664574"
131+
},
132+
{
133+
"name": "Bob",
134+
"id": "fb52a80f-8982-46be-bcdd-605932d8ef03",
135+
"created_at": "2023-11-13T12:29:18.664593"
136+
}
137+
]
138+
```
139+
140+
Send an HTTP POST request to add a new user:
141+
142+
```shell
143+
$ curl -s -X POST http://127.0.0.1:5000/users \
144+
-H 'Content-Type: application/json' \
145+
--data '{"name": "Frank"}' | jq
146+
{
147+
"name": "Frank",
148+
"id": "f6d3cae7-f86a-4bc8-8d05-2fb65e8c6f3b",
149+
"created_at": "2023-11-13T12:31:21.602389"
150+
}
151+
```
152+
153+
#### Django REST Framework
154+
155+
Navigate to the folder:
156+
157+
```shell
158+
(venv) $ cd http-payload/django-rest-api/
159+
```
160+
161+
Apply the migrations if necessary:
162+
163+
```shell
164+
(venv) $ python manage.py migrate
165+
```
166+
167+
Start the Django development web server:
168+
169+
```shell
170+
(venv) $ python manage.py runserver
171+
```
172+
173+
Navigate to the "users" resource in your web browser:
174+
<http://127.0.0.1:8000/users/>
175+
176+
You can use the web interface generated by Django REST Framework to send a POST request to add a new user, for example:
177+
178+
```json
179+
{"name": "Frank"}
180+
```
181+
182+
#### FastAPI
183+
184+
Start the web server:
185+
186+
```shell
187+
(venv) $ cd http-payload/fastapi-rest-api/
188+
(venv) $ uvicorn main:app --reload
189+
```
190+
191+
Navigate to the "users" resource in your web browser:
192+
<http://127.0.0.1:8000/users>
193+
194+
Send an HTTP GET request to retrieve all users:
195+
196+
```shell
197+
$ curl -s http://127.0.0.1:8000/users | jq
198+
[
199+
{
200+
"name": "Alice",
201+
"id": "512a956f-165a-429f-9ec8-83d859843072",
202+
"created_at": "2023-11-13T12:29:18.664574"
203+
},
204+
{
205+
"name": "Bob",
206+
"id": "fb52a80f-8982-46be-bcdd-605932d8ef03",
207+
"created_at": "2023-11-13T12:29:18.664593"
208+
}
209+
]
210+
```
211+
212+
Send an HTTP POST request to add a new user:
213+
214+
```shell
215+
$ curl -s -X POST http://127.0.0.1:8000/users \
216+
-H 'Content-Type: application/json' \
217+
--data '{"name": "Frank"}' | jq
218+
{
219+
"name": "Frank",
220+
"id": "f6d3cae7-f86a-4bc8-8d05-2fb65e8c6f3b",
221+
"created_at": "2023-11-13T12:31:21.602389"
222+
}
223+
```
224+
225+
#### Pydantic
226+
227+
Start the FastAPI server:
228+
229+
```shell
230+
(venv) $ cd http-payload/fastapi-rest-api/
231+
(venv) $ uvicorn main:app --reload
232+
```
233+
234+
Run the REST API consumer:
235+
236+
```shell
237+
(venv) $ cd http-payload/pydantic-demo/
238+
(venv) $ python main.py
239+
```
240+
241+
### Hierarchical Data
242+
243+
#### XML, YAML, JSON, BSON
244+
245+
```shell
246+
(venv) $ cd hierarchical-data/
247+
(venv) $ python bson_demo.py
248+
(venv) $ python yaml_demo.py
249+
```
250+
251+
### Tabular Data
252+
253+
#### CSV
254+
255+
```shell
256+
(venv) $ cd tabular-data/csv-demo/
257+
(venv) $ python main.py
258+
```
259+
260+
#### Apache Parquet
261+
262+
```shell
263+
(venv) $ cd tabular-data/parquet-demo/
264+
(venv) $ python main.py
265+
```
266+
267+
### Schema-Based Formats
268+
269+
#### Apache Avro
270+
271+
```shell
272+
(venv) $ cd schema-based/avro-demo/
273+
(venv) $ python main.py
274+
```
275+
276+
#### Protocol Buffers (Protobuf)
277+
278+
Install the `protoc` compiler:
279+
280+
```shell
281+
$ sudo apt install protobuf-compiler
282+
```
283+
284+
Generate Python code from IDL:
285+
286+
```shell
287+
(venv) $ cd schema-based/protocol-buffers-demo/
288+
(venv) $ protoc --python_out=. --pyi_out=. users.proto
289+
```
290+
291+
Run the demo:
292+
293+
```shell
294+
(venv) $ python main.py
295+
```
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import dill
2+
3+
4+
def main():
5+
create_plus = deserialize(serialize())
6+
print(create_plus)
7+
print(f"{create_plus(3)(2) = }") # noqa
8+
9+
10+
def serialize():
11+
import plus
12+
13+
plus.create_plus.__module__ = None
14+
return dill.dumps(plus.create_plus, recurse=True)
15+
16+
17+
def deserialize(data):
18+
return dill.loads(data)
19+
20+
21+
if __name__ == "__main__":
22+
main()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
def create_plus(x):
2+
def plus(y):
3+
return x + y
4+
5+
return plus
6+
7+
8+
plus_one = create_plus(1)
9+
plus_two = lambda x: x + 2 # noqa
381 Bytes
Binary file not shown.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import pickle
2+
from pathlib import Path
3+
4+
from trustworthy import safe_dump, safe_load
5+
6+
7+
def main():
8+
path = Path("code.pkl")
9+
serialize(lambda a, b: a + b, path, b"top-secret")
10+
code = deserialize(path, b"top-secret")
11+
print(code)
12+
print(f"{code(3, 2) = }") # noqa
13+
try:
14+
deserialize(path, b"incorrect-key")
15+
except pickle.UnpicklingError as ex:
16+
print(repr(ex))
17+
18+
19+
def serialize(code, path, secret_key):
20+
with path.open(mode="wb") as file:
21+
safe_dump(code, file, secret_key)
22+
23+
24+
def deserialize(path, secret_key):
25+
with path.open(mode="rb") as file:
26+
return safe_load(file, secret_key)
27+
28+
29+
if __name__ == "__main__":
30+
main()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import importlib
2+
import io
3+
import pickle
4+
5+
6+
class SafeUnpickler(pickle.Unpickler):
7+
ALLOWED = {
8+
"builtins": ["print"],
9+
"sysconfig": ["get_python_version"],
10+
}
11+
12+
@classmethod
13+
def safe_loads(cls, serialized_data):
14+
file = io.BytesIO(serialized_data)
15+
return cls(file).load()
16+
17+
def find_class(self, module_name, name):
18+
if module_name in self.ALLOWED:
19+
if name in self.ALLOWED[module_name]:
20+
module = importlib.import_module(module_name)
21+
return getattr(module, name)
22+
raise pickle.UnpicklingError(f"{module_name}.{name} is unsafe")

0 commit comments

Comments
 (0)