From 4454c1be9d29319519c19d23a7a59cd29bf83c3f Mon Sep 17 00:00:00 2001
From: drpsyko101
Date: Sat, 9 Nov 2024 16:55:07 +0800
Subject: [PATCH 1/4] Allow queries by model's field
* All requests now can use any fields in the model except for binary and datetime
* Fields now support wildcard (*) to output all model fields
* GET request will return ID field if no fields are given
* Return proper HTTP error codes and messages in JSON format by default
---
rest_api_odoo/controllers/main.py | 244 ++++++++++----------
rest_api_odoo/static/description/index.html | 51 ++--
2 files changed, 142 insertions(+), 153 deletions(-)
diff --git a/rest_api_odoo/controllers/main.py b/rest_api_odoo/controllers/main.py
index 37169cd3b..20b057bc6 100644
--- a/rest_api_odoo/controllers/main.py
+++ b/rest_api_odoo/controllers/main.py
@@ -23,7 +23,8 @@
import json
import logging
from odoo import http
-from odoo.http import request
+from odoo.http import request, Response
+from ast import literal_eval
_logger = logging.getLogger(__name__)
@@ -38,80 +39,83 @@ class RestApi(http.Controller):
user_id = request.env['res.users'].search([('api_key', '=', api_key)])
if api_key is not None and user_id:
- response = True
+ return True
elif not user_id:
- response = ('Invalid API Key '
- '!
')
- else:
- response = ("No API Key Provided "
- "!
")
-
- return response
+ return Response(json.dumps({'message': 'Invalid API Key'}), status=401)
+ return Response(json.dumps({'message': 'No API Key Provided'}), status=400)
+
+ def simplest_type(self, input):
+ """Try cast input into native Python class, otherwise return as string"""
+ try:
+ return literal_eval(input)
+ except:
+ if input == 'true':
+ return True
+ if input == 'false':
+ return False
+ return input
- def generate_response(self, method, model, rec_id):
+ def generate_response(self, method, **query):
"""This function is used to generate the response based on the type
of request and the parameters given"""
+ model = query.pop('model')
option = request.env['connection.api'].search(
[('model_id', '=', model)], limit=1)
model_name = option.model_id.model
+ model_display_name = option.model_id.name
- if method != 'DELETE':
+ data = {}
+ try:
data = json.loads(request.httprequest.data)
- else:
- data = {}
+ except:
+ pass
+
fields = []
if data:
for field in data['fields']:
fields.append(field)
- if not fields and method != 'DELETE':
- return ("No fields selected for the model"
- "
")
+ if '*' in fields:
+ fields = []
+ record_fields = request.env[
+ str(model_name)].fields_get([], attributes=['type'])
+ for field, value in record_fields.items():
+ value_type = value.get('type')
+ if not (value_type == 'binary' or value_type == 'datetime'):
+ fields.append(field)
if not option:
- return ("No Record Created for the model"
- "
")
+ return Response(json.dumps({'message': f'No Record Created for the model. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}), status=403)
try:
if method == 'GET':
- fields = []
- for field in data['fields']:
- fields.append(field)
+ if not fields:
+ fields.append('id')
if not option.is_get:
- return ("Method Not Allowed"
- "
")
+ return Response(
+ json.dumps({'message': f'Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}),
+ status=405)
else:
- datas = []
- if rec_id != 0:
- partner_records = request.env[
- str(model_name)].search_read(
- domain=[('id', '=', rec_id)],
- fields=fields
- )
- data = json.dumps({
- 'records': partner_records
- })
- datas.append(data)
- return request.make_response(data=datas)
- else:
- partner_records = request.env[
- str(model_name)].search_read(
- domain=[],
- fields=fields
- )
- data = json.dumps({
+ domains = []
+ for key, value in query.items():
+ domains.append((key, '=', self.simplest_type(value)))
+ partner_records = request.env[
+ str(model_name)].search_read(
+ domain=domains,
+ fields=fields
+ )
+ return Response(
+ json.dumps({
'records': partner_records
})
- datas.append(data)
- return request.make_response(data=datas)
- except:
- return ("Invalid JSON Data"
- "
")
- if method == 'POST':
- if not option.is_post:
- return ("Method Not Allowed"
- "
")
- else:
+ )
+ if method == 'POST':
+ if not option.is_post:
+ return Response(
+ json.dumps({'message': f'Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}),
+ status=405)
+ if not data or 'values' not in data:
+ return Response(json.dumps({'message': 'No Data Provided'}), status=403)
+
try:
data = json.loads(request.httprequest.data)
- datas = []
new_resource = request.env[str(model_name)].create(
data['values'])
partner_records = request.env[
@@ -119,71 +123,61 @@ class RestApi(http.Controller):
domain=[('id', '=', new_resource.id)],
fields=fields
)
- new_data = json.dumps({'New resource': partner_records, })
- datas.append(new_data)
- return request.make_response(data=datas)
+ return Response(json.dumps({'new_record': partner_records}), status=201)
except:
- return ("Invalid JSON Data"
- "
")
- if method == 'PUT':
- if not option.is_put:
- return ("Method Not Allowed"
- "
")
- else:
- if rec_id == 0:
- return ("No ID Provided"
- "
")
- else:
- resource = request.env[str(model_name)].browse(
- int(rec_id))
- if not resource.exists():
- return ("Resource not found"
- "
")
- else:
- try:
- datas = []
- data = json.loads(request.httprequest.data)
- resource.write(data['values'])
- partner_records = request.env[
- str(model_name)].search_read(
- domain=[('id', '=', resource.id)],
- fields=fields
- )
- new_data = json.dumps(
- {'Updated resource': partner_records,
- })
- datas.append(new_data)
- return request.make_response(data=datas)
-
- except:
- return ("Invalid JSON Data "
- "!
")
- if method == 'DELETE':
- if not option.is_delete:
- return ("Method Not Allowed"
- "
")
- else:
- if rec_id == 0:
- return ("No ID Provided"
- "
")
+ return Response(json.dumps({'message': 'Invalid JSON Data'}), status=403)
+ if method == 'PUT':
+ if not option.is_put:
+ return Response(
+ json.dumps({'message': f'Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}),
+ status=405)
+
+ if not 'id' in query:
+ return Response(json.dumps({'message': 'No ID Provided'}), status=403)
+ if not data or 'values' not in data:
+ return Response(json.dumps({'message': 'No Data Provided'}), status=403)
+
+ resource = request.env[str(model_name)].browse(
+ int(query.get('id')))
+ if not resource.exists():
+ return Response(json.dumps({'message': 'Resource not found'}), status=404)
+
+ try:
+ data = json.loads(request.httprequest.data)
+ resource.write(data['values'])
+ partner_records = request.env[
+ str(model_name)].search_read(
+ domain=[('id', '=', resource.id)],
+ fields=fields
+ )
+ return Response(json.dumps({'updated_record': partner_records}))
+
+ except:
+ return Response(json.dumps({'message': 'Invalid JSON value(s) passed'}), status=403)
+ if method == 'DELETE':
+ if not option.is_delete:
+ return Response(
+ json.dumps({'message': f'Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}),
+ status=405)
+
+ if not 'id' in query:
+ return Response(json.dumps({'message': 'No ID Provided'}), status=403)
+
+ resource = request.env[str(model_name)].browse(
+ int(query.get('id')))
+ if not resource.exists():
+ return Response(json.dumps({'message': 'Resource not found'}), status=404)
else:
- resource = request.env[str(model_name)].browse(
- int(rec_id))
- if not resource.exists():
- return ("Resource not found"
- "
")
- else:
-
- records = request.env[
- str(model_name)].search_read(
- domain=[('id', '=', resource.id)],
- fields=['id', 'display_name']
- )
- remove = json.dumps(
- {"Resource deleted": records,
- })
- resource.unlink()
- return request.make_response(data=remove)
+
+ records = request.env[
+ str(model_name)].search_read(
+ domain=[('id', '=', resource.id)],
+ fields=fields
+ )
+ resource.unlink()
+ return Response(json.dumps({'message': 'Resource deleted', 'data': records}), status=202)
+ except:
+ return Response(json.dumps({'message': 'Internal Server Error'}), status=500)
@http.route(['/send_request'], type='http',
auth='none',
@@ -196,7 +190,7 @@ class RestApi(http.Controller):
http_method = request.httprequest.method
api_key = request.httprequest.headers.get('api-key')
auth_api = self.auth_api_key(api_key)
- model = kw.get('model')
+ model = kw.pop('model')
username = request.httprequest.headers.get('login')
password = request.httprequest.headers.get('password')
request.session.authenticate(request.session.db, username,
@@ -204,17 +198,12 @@ class RestApi(http.Controller):
model_id = request.env['ir.model'].search(
[('model', '=', model)])
if not model_id:
- return ("Invalid model, check spelling or maybe "
- "the related "
- "module is not installed"
- "
")
+ return Response(json.dumps(
+ {'message': 'Invalid model, check spelling or maybe the related module is not installed'}),
+ status=403)
if auth_api == True:
- if not kw.get('Id'):
- rec_id = 0
- else:
- rec_id = int(kw.get('Id'))
- result = self.generate_response(http_method, model_id.id, rec_id)
+ result = self.generate_response(http_method, model=model_id.id, **kw)
return result
else:
return auth_api
@@ -239,5 +228,4 @@ class RestApi(http.Controller):
"api-key": api_key})
return request.make_response(data=datas)
except:
- return ("wrong login credentials"
- "
")
+ return Response(json.dumps({'message': 'wrong login credentials'}), status=401)
diff --git a/rest_api_odoo/static/description/index.html b/rest_api_odoo/static/description/index.html
index bf41854b7..11a198179 100644
--- a/rest_api_odoo/static/description/index.html
+++ b/rest_api_odoo/static/description/index.html
@@ -217,7 +217,7 @@
First of all, we have to add a new parameter in odoo conf.
file.
- server_wide_modules = web, base, rest_api_odoo
+ server_wide_modules = web, base, rest_api_odoo
- This will allow us to send request to server without
selecting database first.
- Incase if you have to
uninstall the module , you have to remove this parameter.
@@ -251,10 +251,10 @@
Next you have to select the database and login.
We have attached Postman collections through which
you can
- authenticate rest api.
+ authenticate REST api.
First, extract the zip file. Then, you will obtain the JSON-format file, which you can directly import into POSTMAN.
- The url format will be like this - http://cybrosys:8016/odoo_connect
+ The url format will be like this - http://cybrosys:8016/odoo_connect
Replace 'cybrosys:8016' with your localhost port number.
You have to provide database name, username and password
@@ -266,9 +266,9 @@
This key will be used when sending api requests to
database.
- The response will be like this - {"Status": "auth
+ The response will be like this - {"Status": "auth
successful", "User": "Mitchell Admin", "api-key":
- "66c2ebab-d4dc-42f0-87d0-d1646e887569"}.
+ "66c2ebab-d4dc-42f0-87d0-d1646e887569"}.
- - You can send GET request to retrieve data from the
+
- You can send
GET
request to retrieve data from the
database.
- The postman collection has been provided with app files for
@@ -318,15 +318,16 @@
- Model can be passed as argument as the technical name , and
also if you want
- specific record you can provide the id as well.
+ specific record you can provide any related fields to the module as well.
- - The format for GET method will be like this - http://cybrosys:8016/send_request?model=res.partner&Id=10.
+
- The format for GET method will be like this -
http://cybrosys:8016/send_request?model=res.partner&Id=10.
- We can specify the fields inside the JSON data, and it will
- be like this - {"fields": ["name", "email"]}.
- - This is the format of api response - {"records": [{"id":
+ be like this -
{"fields": ["name", "email"]}
.
+ - This is the format of api response -
{"records": [{"id":
10, "email": "deco.addict82@example.com", "name": "Deco
- Addict"}]}.
+ Addict"}]}
. If no fields are passed , it will returns
+ just the record's ID. To get all of the fields, set the fields to wildcard - {"fields": ["*"]}
.
@@ -338,7 +339,7 @@
- - Using POST method , you can create new records in the
+
- Using
POST
method , you can create new records in the
database.
- Just make sure you enabled POST method for the model record
@@ -351,18 +352,18 @@
- You can make use of the postman collection that we have
added with app files.
- - The format for sending POST request will be like this - http://cybrosys:8016/send_request?model=res.partner.
+
- The format for sending POST request will be like this -
http://cybrosys:8016/send_request?model=res.partner.
- - This is the format for JSON data - {
+
- This is the format for JSON data -
{
"fields" :["name", "phone"] ,
"values": {"name": "abc",
"phone":"55962441552"
- } }.
+ } }.
- Make sure the data entered in correct format otherwise you
will get 'Invalid JSON data' message.
- - Response will be in this format - {"New resource":
- [{"id": 51, "name": "abc", "phone": "55962441552"}]}.
+
- Response will be in this format -
{"New resource":
+ [{"id": 51, "name": "abc", "phone": "55962441552"}]}
.
@@ -373,7 +374,7 @@
Update Records
- - Updation of records in the database can be done with PUT
+
- Updation of records in the database can be done with
PUT
method.
- You have to provide the model and also the id or the record
@@ -383,14 +384,14 @@
will be always have to send request with your login
credentials. Otherwise, it will be showing access denied.
- - The format for sending PUT request will be like this - http://cybrosys:8016/send_request?model=res.partner&Id=46.
+
- The format for sending PUT request will be like this -
http://cybrosys:8016/send_request?model=res.partner&Id=46.
- Here too you have to provide the JSON data through which the
updates will be done.
- - The response format will be like this - {"Updated
+
- The response format will be like this -
{"Updated
resource": [{"id": 46, "email": "abc@example.com", "name":
- "Toni"}]}.
+ "Toni"}]}.
@@ -401,7 +402,7 @@
- - Database records can be deleted by sending DELETE method
+
- Database records can be deleted by sending
DELETE
method
request.
- For the deletion we have to provide the Model and the record
@@ -410,11 +411,11 @@
- Make sure you have permission to delete files for the
selected model in the rest api record.
- - The delete request format will be like this - http://cybrosys:8016/send_request?model=res.partner&Id=46.
+
- The delete request format will be like this -
http://cybrosys:8016/send_request?model=res.partner&Id=46.
- - The response after successful deletion will be -
+
- The response after successful deletion will be -
{"Resource deleted": [{"id": 46, "email": "abc@example.com",
- "name": "Toni"}]}.
+ "name": "Toni"}]}.
From d9611724c28b943730789771bdbc258a71969d1f Mon Sep 17 00:00:00 2001
From: drpsyko101
Date: Sun, 10 Nov 2024 23:42:39 +0800
Subject: [PATCH 2/4] Include datetime object in return field
* Fix no fields returned by passing ID if no field is specified
---
rest_api_odoo/controllers/main.py | 356 ++++++++++++++++++------------
1 file changed, 211 insertions(+), 145 deletions(-)
diff --git a/rest_api_odoo/controllers/main.py b/rest_api_odoo/controllers/main.py
index 20b057bc6..78706b671 100644
--- a/rest_api_odoo/controllers/main.py
+++ b/rest_api_odoo/controllers/main.py
@@ -22,6 +22,8 @@
import json
import logging
+
+from datetime import datetime
from odoo import http
from odoo.http import request, Response
from ast import literal_eval
@@ -37,195 +39,259 @@ class RestApi(http.Controller):
"""This function is used to authenticate the api-key when sending a
request"""
- user_id = request.env['res.users'].search([('api_key', '=', api_key)])
+ user_id = request.env["res.users"].search([("api_key", "=", api_key)])
if api_key is not None and user_id:
- return True
+ return Response(json.dumps({"message": "Authorized"}), status=200)
elif not user_id:
- return Response(json.dumps({'message': 'Invalid API Key'}), status=401)
- return Response(json.dumps({'message': 'No API Key Provided'}), status=400)
-
+ return Response(json.dumps({"message": "Invalid API Key"}), status=401)
+ return Response(json.dumps({"message": "No API Key Provided"}), status=400)
+
def simplest_type(self, input):
"""Try cast input into native Python class, otherwise return as string"""
try:
return literal_eval(input)
- except:
- if input == 'true':
+ except Exception:
+ # Handle lowercase booleans
+ if input == "true":
return True
- if input == 'false':
+ if input == "false":
return False
return input
+ def sanitize_records(self, records):
+ """Sanitize records for response"""
+ for record in records:
+ for key, value in record.items():
+ # Manually convert datetime fields to string format
+ if isinstance(value, datetime):
+ record[key] = value.isoformat()
+ return records
+
def generate_response(self, method, **query):
"""This function is used to generate the response based on the type
of request and the parameters given"""
- model = query.pop('model')
- option = request.env['connection.api'].search(
- [('model_id', '=', model)], limit=1)
+ model = query.pop("model")
+ option = request.env["connection.api"].search(
+ [("model_id", "=", model)], limit=1
+ )
model_name = option.model_id.model
model_display_name = option.model_id.name
- data = {}
try:
data = json.loads(request.httprequest.data)
- except:
- pass
+ except Exception:
+ data = {}
fields = []
if data:
- for field in data['fields']:
- fields.append(field)
- if '*' in fields:
+ for field in data["fields"]:
+ fields.append(field)
+
+ # Return records' ID by default if not specified
+ if not fields:
+ fields.append("id")
+
+ # Get all model's fields if wildcard is used
+ if "*" in fields:
fields = []
- record_fields = request.env[
- str(model_name)].fields_get([], attributes=['type'])
+ record_fields = request.env[str(model_name)].fields_get(
+ [], attributes=["type"]
+ )
for field, value in record_fields.items():
- value_type = value.get('type')
- if not (value_type == 'binary' or value_type == 'datetime'):
+ value_type = value.get("type")
+ if not (value_type == "binary"):
fields.append(field)
if not option:
- return Response(json.dumps({'message': f'No Record Created for the model. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}), status=403)
- try:
- if method == 'GET':
- if not fields:
- fields.append('id')
- if not option.is_get:
- return Response(
- json.dumps({'message': f'Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}),
- status=405)
- else:
- domains = []
- for key, value in query.items():
- domains.append((key, '=', self.simplest_type(value)))
- partner_records = request.env[
- str(model_name)].search_read(
- domain=domains,
- fields=fields
- )
- return Response(
- json.dumps({
- 'records': partner_records
- })
- )
- if method == 'POST':
- if not option.is_post:
- return Response(
- json.dumps({'message': f'Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}),
- status=405)
- if not data or 'values' not in data:
- return Response(json.dumps({'message': 'No Data Provided'}), status=403)
-
- try:
- data = json.loads(request.httprequest.data)
- new_resource = request.env[str(model_name)].create(
- data['values'])
- partner_records = request.env[
- str(model_name)].search_read(
- domain=[('id', '=', new_resource.id)],
- fields=fields
- )
- return Response(json.dumps({'new_record': partner_records}), status=201)
- except:
- return Response(json.dumps({'message': 'Invalid JSON Data'}), status=403)
- if method == 'PUT':
- if not option.is_put:
- return Response(
- json.dumps({'message': f'Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}),
- status=405)
-
- if not 'id' in query:
- return Response(json.dumps({'message': 'No ID Provided'}), status=403)
- if not data or 'values' not in data:
- return Response(json.dumps({'message': 'No Data Provided'}), status=403)
-
- resource = request.env[str(model_name)].browse(
- int(query.get('id')))
- if not resource.exists():
- return Response(json.dumps({'message': 'Resource not found'}), status=404)
-
- try:
- data = json.loads(request.httprequest.data)
- resource.write(data['values'])
- partner_records = request.env[
- str(model_name)].search_read(
- domain=[('id', '=', resource.id)],
- fields=fields
- )
- return Response(json.dumps({'updated_record': partner_records}))
-
- except:
- return Response(json.dumps({'message': 'Invalid JSON value(s) passed'}), status=403)
- if method == 'DELETE':
- if not option.is_delete:
- return Response(
- json.dumps({'message': f'Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record.'}),
- status=405)
-
- if not 'id' in query:
- return Response(json.dumps({'message': 'No ID Provided'}), status=403)
-
- resource = request.env[str(model_name)].browse(
- int(query.get('id')))
- if not resource.exists():
- return Response(json.dumps({'message': 'Resource not found'}), status=404)
- else:
-
- records = request.env[
- str(model_name)].search_read(
- domain=[('id', '=', resource.id)],
- fields=fields
+ return Response(
+ json.dumps(
+ {
+ "message": f"No Record Created for the model. Please contact your admininstrator to enable {method} method for {model_display_name} record."
+ }
+ ),
+ status=403,
+ )
+ if method == "GET":
+ if not option.is_get:
+ return Response(
+ json.dumps(
+ {
+ "message": f"Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record."
+ }
+ ),
+ status=405,
+ )
+
+ domains = []
+ for key, value in query.items():
+ domains.append((key, "=", self.simplest_type(value)))
+ partner_records = request.env[str(model_name)].search_read(
+ domain=domains, fields=fields
+ )
+
+ return Response(
+ json.dumps({"records": self.sanitize_records(partner_records)})
+ )
+ if method == "POST":
+ if not option.is_post:
+ return Response(
+ json.dumps(
+ {
+ "message": f"Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record."
+ }
+ ),
+ status=405,
+ )
+ if not data or "values" not in data:
+ return Response(json.dumps({"message": "No Data Provided"}), status=403)
+
+ try:
+ data = json.loads(request.httprequest.data)
+ new_resource = request.env[str(model_name)].create(data["values"])
+ partner_records = request.env[str(model_name)].search_read(
+ domain=[("id", "=", new_resource.id)], fields=fields
+ )
+ return Response(
+ json.dumps({"new_record": self.sanitize_records(partner_records)}),
+ status=201,
+ )
+ except Exception:
+ return Response(
+ json.dumps({"message": "Invalid JSON Data"}), status=403
+ )
+ if method == "PUT":
+ if not option.is_put:
+ return Response(
+ json.dumps(
+ {
+ "message": f"Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record."
+ }
+ ),
+ status=405,
+ )
+
+ if "id" not in query:
+ return Response(json.dumps({"message": "No ID Provided"}), status=403)
+ if not data or "values" not in data:
+ return Response(json.dumps({"message": "No Data Provided"}), status=403)
+
+ resource_id = str(query.get("id"))
+ resource = request.env[str(model_name)].browse(int(resource_id))
+ if not resource.exists():
+ return Response(
+ json.dumps({"message": "Resource not found"}), status=404
+ )
+
+ try:
+ data = json.loads(request.httprequest.data)
+ resource.write(data["values"])
+ partner_records = request.env[str(model_name)].search_read(
+ domain=[("id", "=", resource.id)], fields=fields
+ )
+ return Response(
+ json.dumps(
+ {"updated_record": self.sanitize_records(partner_records)}
)
- resource.unlink()
- return Response(json.dumps({'message': 'Resource deleted', 'data': records}), status=202)
- except:
- return Response(json.dumps({'message': 'Internal Server Error'}), status=500)
-
- @http.route(['/send_request'], type='http',
- auth='none',
- methods=['GET', 'POST', 'PUT', 'DELETE'], csrf=False)
+ )
+
+ except Exception:
+ return Response(
+ json.dumps({"message": "Invalid JSON value(s) passed"}),
+ status=403,
+ )
+ if method == "DELETE":
+ if not option.is_delete:
+ return Response(
+ json.dumps(
+ {
+ "message": f"Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record."
+ }
+ ),
+ status=405,
+ )
+
+ if "id" not in query:
+ return Response(json.dumps({"message": "No ID Provided"}), status=403)
+
+ resource_id = str(query.get("id"))
+ resource = request.env[str(model_name)].browse(int(resource_id))
+ if not resource.exists():
+ return Response(
+ json.dumps({"message": "Resource not found"}), status=404
+ )
+
+ partner_records = request.env[str(model_name)].search_read(
+ domain=[("id", "=", resource.id)], fields=fields
+ )
+ resource.unlink()
+ return Response(
+ json.dumps(
+ {
+ "message": "Resource deleted",
+ "data": self.sanitize_records(partner_records),
+ }
+ ),
+ status=202,
+ )
+
+ # If not using any method above, simply return an error
+ return Response(json.dumps({"message": "Method not allowed"}), status=405)
+
+ @http.route(
+ ["/send_request"],
+ type="http",
+ auth="none",
+ methods=["GET", "POST", "PUT", "DELETE"],
+ csrf=False,
+ )
def fetch_data(self, **kw):
"""This controller will be called when sending a request to the
specified url, and it will authenticate the api-key and then will
generate the result"""
http_method = request.httprequest.method
- api_key = request.httprequest.headers.get('api-key')
+ api_key = request.httprequest.headers.get("api-key")
auth_api = self.auth_api_key(api_key)
- model = kw.pop('model')
- username = request.httprequest.headers.get('login')
- password = request.httprequest.headers.get('password')
- request.session.authenticate(request.session.db, username,
- password)
- model_id = request.env['ir.model'].search(
- [('model', '=', model)])
+ model = kw.pop("model")
+ username = request.httprequest.headers.get("login")
+ password = request.httprequest.headers.get("password")
+ request.session.authenticate(request.session.db, username, password)
+ model_id = request.env["ir.model"].search([("model", "=", model)])
if not model_id:
- return Response(json.dumps(
- {'message': 'Invalid model, check spelling or maybe the related module is not installed'}),
- status=403)
+ return Response(
+ json.dumps(
+ {
+ "message": "Invalid model, check spelling or maybe the related module is not installed"
+ }
+ ),
+ status=403,
+ )
- if auth_api == True:
+ if auth_api.status_code == 200:
result = self.generate_response(http_method, model=model_id.id, **kw)
return result
else:
return auth_api
- @http.route(['/odoo_connect'], type="http", auth="none", csrf=False,
- methods=['GET'])
+ @http.route(
+ ["/odoo_connect"], type="http", auth="none", csrf=False, methods=["GET"]
+ )
def odoo_connect(self, **kw):
"""This is the controller which initializes the api transaction by
generating the api-key for specific user and database"""
- username = request.httprequest.headers.get('login')
- password = request.httprequest.headers.get('password')
- db = request.httprequest.headers.get('db')
+ username = request.httprequest.headers.get("login")
+ password = request.httprequest.headers.get("password")
+ db = request.httprequest.headers.get("db")
try:
request.session.update(http.get_default_session(), db=db)
- auth = request.session.authenticate(request.session.db, username,
- password)
- user = request.env['res.users'].browse(auth)
+ auth = request.session.authenticate(request.session.db, username, password)
+ user = request.env["res.users"].browse(auth)
api_key = request.env.user.generate_api(username)
- datas = json.dumps({"Status": "auth successful",
- "User": user.name,
- "api-key": api_key})
- return request.make_response(data=datas)
- except:
- return Response(json.dumps({'message': 'wrong login credentials'}), status=401)
+ datas = json.dumps(
+ {"Status": "auth successful", "User": user.name, "api-key": api_key}
+ )
+ return Response(datas, status=200)
+ except Exception:
+ return Response(
+ json.dumps({"message": "wrong login credentials"}), status=401
+ )
From fa298553b4f3cde1d26bf6f69999a5d370dcd237 Mon Sep 17 00:00:00 2001
From: drpsyko101
Date: Mon, 11 Nov 2024 22:52:10 +0800
Subject: [PATCH 3/4] Add pagination support
* Simplify error handling for all methods
---
rest_api_odoo/controllers/main.py | 212 +++++++++-----------
rest_api_odoo/static/description/index.html | 10 +-
2 files changed, 99 insertions(+), 123 deletions(-)
diff --git a/rest_api_odoo/controllers/main.py b/rest_api_odoo/controllers/main.py
index 78706b671..0f02df40b 100644
--- a/rest_api_odoo/controllers/main.py
+++ b/rest_api_odoo/controllers/main.py
@@ -70,171 +70,143 @@ class RestApi(http.Controller):
def generate_response(self, method, **query):
"""This function is used to generate the response based on the type
of request and the parameters given"""
- model = query.pop("model")
- option = request.env["connection.api"].search(
- [("model_id", "=", model)], limit=1
- )
- model_name = option.model_id.model
- model_display_name = option.model_id.name
-
try:
- data = json.loads(request.httprequest.data)
- except Exception:
- data = {}
+ model = query.pop("model")
+ option = request.env["connection.api"].search(
+ [("model_id", "=", model)], limit=1
+ )
+ model_name = option.model_id.model
+ model_display_name = option.model_id.name
- fields = []
- if data:
- for field in data["fields"]:
- fields.append(field)
-
- # Return records' ID by default if not specified
- if not fields:
- fields.append("id")
+ try:
+ data = json.loads(request.httprequest.data)
+ except Exception:
+ data = {}
- # Get all model's fields if wildcard is used
- if "*" in fields:
fields = []
- record_fields = request.env[str(model_name)].fields_get(
- [], attributes=["type"]
- )
- for field, value in record_fields.items():
- value_type = value.get("type")
- if not (value_type == "binary"):
+ if data:
+ for field in data["fields"]:
fields.append(field)
- if not option:
- return Response(
- json.dumps(
- {
- "message": f"No Record Created for the model. Please contact your admininstrator to enable {method} method for {model_display_name} record."
- }
- ),
- status=403,
- )
- if method == "GET":
- if not option.is_get:
- return Response(
- json.dumps(
- {
- "message": f"Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record."
- }
- ),
- status=405,
+
+ # Return records' ID by default if not specified
+ if not fields:
+ fields.append("id")
+
+ # Get all model's fields if wildcard is used
+ if "*" in fields:
+ fields = []
+ record_fields = request.env[str(model_name)].fields_get(
+ [], attributes=["type"]
)
+ for field, value in record_fields.items():
+ value_type = value.get("type")
+ if not (value_type == "binary"):
+ fields.append(field)
+ if not option:
+ raise NotImplementedError("No Record Created for the model. ")
+ if method == "GET":
+ if not option.is_get:
+ raise NameError()
+ limit = 0
+ if query.get("limit"):
+ limit = int(str(query.get("limit")))
+ offset = 0
+ if query.get("offset"):
+ offset = int(str(query.get("offset")))
- domains = []
- for key, value in query.items():
- domains.append((key, "=", self.simplest_type(value)))
- partner_records = request.env[str(model_name)].search_read(
- domain=domains, fields=fields
- )
+ domains = []
+ for key, value in query.items():
+ if not (key == "limit" or key == "offset"):
+ domains.append((key, "=", self.simplest_type(value)))
+ partner_records = request.env[str(model_name)].search_read(
+ domains, fields, limit=limit, offset=offset
+ )
- return Response(
- json.dumps({"records": self.sanitize_records(partner_records)})
- )
- if method == "POST":
- if not option.is_post:
return Response(
- json.dumps(
- {
- "message": f"Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record."
- }
- ),
- status=405,
+ json.dumps({"records": self.sanitize_records(partner_records)})
)
- if not data or "values" not in data:
- return Response(json.dumps({"message": "No Data Provided"}), status=403)
+ if method == "POST":
+ if not option.is_post:
+ raise NotImplementedError()
+ if not data or "values" not in data:
+ raise ValueError("No Data Provided")
- try:
data = json.loads(request.httprequest.data)
new_resource = request.env[str(model_name)].create(data["values"])
partner_records = request.env[str(model_name)].search_read(
- domain=[("id", "=", new_resource.id)], fields=fields
+ [("id", "=", new_resource.id)], fields
)
return Response(
json.dumps({"new_record": self.sanitize_records(partner_records)}),
status=201,
)
- except Exception:
- return Response(
- json.dumps({"message": "Invalid JSON Data"}), status=403
- )
- if method == "PUT":
- if not option.is_put:
- return Response(
- json.dumps(
- {
- "message": f"Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record."
- }
- ),
- status=405,
- )
+ if method == "PUT":
+ if not option.is_put:
+ raise NotImplementedError()
- if "id" not in query:
- return Response(json.dumps({"message": "No ID Provided"}), status=403)
- if not data or "values" not in data:
- return Response(json.dumps({"message": "No Data Provided"}), status=403)
+ if "id" not in query:
+ raise ValueError("No ID Provided")
+ if not data or "values" not in data:
+ raise ValueError("No Data Provided")
- resource_id = str(query.get("id"))
- resource = request.env[str(model_name)].browse(int(resource_id))
- if not resource.exists():
- return Response(
- json.dumps({"message": "Resource not found"}), status=404
- )
+ resource_id = str(query.get("id"))
+ resource = request.env[str(model_name)].browse(int(resource_id))
+ if not resource.exists():
+ raise ValueError("Resource not found")
- try:
data = json.loads(request.httprequest.data)
resource.write(data["values"])
partner_records = request.env[str(model_name)].search_read(
- domain=[("id", "=", resource.id)], fields=fields
+ [("id", "=", resource.id)], fields
)
return Response(
json.dumps(
{"updated_record": self.sanitize_records(partner_records)}
)
)
+ if method == "DELETE":
+ if not option.is_delete:
+ raise NotImplementedError()
- except Exception:
- return Response(
- json.dumps({"message": "Invalid JSON value(s) passed"}),
- status=403,
+ if "id" not in query:
+ raise ValueError("No ID Provided")
+
+ resource_id = str(query.get("id"))
+ resource = request.env[str(model_name)].browse(int(resource_id))
+ if not resource.exists():
+ raise ValueError("Resource not found")
+
+ partner_records = request.env[str(model_name)].search_read(
+ [("id", "=", resource.id)], fields
)
- if method == "DELETE":
- if not option.is_delete:
+ resource.unlink()
return Response(
json.dumps(
{
- "message": f"Method not allowed. Please contact your admininstrator to enable {method} method for {model_display_name} record."
+ "message": "Resource deleted",
+ "data": self.sanitize_records(partner_records),
}
),
- status=405,
- )
-
- if "id" not in query:
- return Response(json.dumps({"message": "No ID Provided"}), status=403)
-
- resource_id = str(query.get("id"))
- resource = request.env[str(model_name)].browse(int(resource_id))
- if not resource.exists():
- return Response(
- json.dumps({"message": "Resource not found"}), status=404
+ status=202,
)
- partner_records = request.env[str(model_name)].search_read(
- domain=[("id", "=", resource.id)], fields=fields
- )
- resource.unlink()
+ # If not using any method above, simply return an error
+ raise NotImplementedError()
+ except ValueError as e:
+ return Response(json.dumps({"message": e.args[0]}), status=403)
+ except NotImplementedError as e:
return Response(
json.dumps(
{
- "message": "Resource deleted",
- "data": self.sanitize_records(partner_records),
+ "message": f"Method not allowed. {e.args[0]}Please contact your admininstrator to enable {method} method for {model_display_name or 'this'} record."
}
),
- status=202,
+ status=405,
+ )
+ except Exception:
+ return Response(
+ json.dumps({"message": "Internal server error"}), status=500
)
-
- # If not using any method above, simply return an error
- return Response(json.dumps({"message": "Method not allowed"}), status=405)
@http.route(
["/send_request"],
@@ -290,7 +262,7 @@ class RestApi(http.Controller):
datas = json.dumps(
{"Status": "auth successful", "User": user.name, "api-key": api_key}
)
- return Response(datas, status=200)
+ return Response(datas)
except Exception:
return Response(
json.dumps({"message": "wrong login credentials"}), status=401
diff --git a/rest_api_odoo/static/description/index.html b/rest_api_odoo/static/description/index.html
index 11a198179..59b955d1e 100644
--- a/rest_api_odoo/static/description/index.html
+++ b/rest_api_odoo/static/description/index.html
@@ -323,11 +323,15 @@
The format for GET method will be like this - http://cybrosys:8016/send_request?model=res.partner&Id=10.
We can specify the fields inside the JSON data, and it will
- be like this - {"fields": ["name", "email"]}
.
+ be like this - {"fields": ["name", "email"]}
.
+ If no fields are passed , it will returns just the record's ID.
+ To get all of the fields, set the fields to wildcard - {"fields": ["*"]}
.
+ Pagination can also be used by adding {"limit": 10, "offset": 0}
+ to the query parameter above.
+
This is the format of api response - {"records": [{"id":
10, "email": "deco.addict82@example.com", "name": "Deco
- Addict"}]}
. If no fields are passed , it will returns
- just the record's ID. To get all of the fields, set the fields to wildcard - {"fields": ["*"]}
.
+ Addict"}]}.
From 8909c65fde8360be036d6c30010bfc3bbe6aafac Mon Sep 17 00:00:00 2001
From: drpsyko101
Date: Sat, 16 Nov 2024 21:27:27 +0800
Subject: [PATCH 4/4] Revert to Odoo API key
* This eliminates the need to create an additional unsecure object in the database.
* Using the API token also enables users with 2FA to authenticate against Odoo server
* Added ACL for the Rest API view to allow access to only certain users
* Removed Postman sample as the new methods has been simplified
* Update the module info to reflect changes above
---
.../Odoo REST Api.postman_collection.zip | Bin 1060 -> 0 bytes
rest_api_odoo/__manifest__.py | 2 +-
rest_api_odoo/controllers/main.py | 98 +++++++++---------
rest_api_odoo/models/__init__.py | 1 -
rest_api_odoo/models/res_users.py | 45 --------
.../security/res_api_odoo_security.xml | 32 ++++++
.../assets/screenshots/screenshot_2.png | Bin 119170 -> 118405 bytes
rest_api_odoo/static/description/index.html | 81 ++++-----------
rest_api_odoo/views/res_users_views.xml | 18 ----
9 files changed, 102 insertions(+), 175 deletions(-)
delete mode 100644 rest_api_odoo/Postman Collections/Odoo REST Api.postman_collection.zip
delete mode 100644 rest_api_odoo/models/res_users.py
create mode 100644 rest_api_odoo/security/res_api_odoo_security.xml
delete mode 100644 rest_api_odoo/views/res_users_views.xml
diff --git a/rest_api_odoo/Postman Collections/Odoo REST Api.postman_collection.zip b/rest_api_odoo/Postman Collections/Odoo REST Api.postman_collection.zip
deleted file mode 100644
index d2a8392f5efb9e7a7d6ad72c5898e189d50fee9e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1060
zcmWIWW@Zs#-~d8}WYus6C`cD#U{GaHVDL}L&sPX?4GvLoEXdR=$S*F*P0Wi=&dGVAUp-yvc%n0QlJ1R}Z@0K={I4x5aLUM3)!I@P`q|0i`44OH`uitWEq43(y>Q4%6
z+$#QmT3Sl<)uxBveQXoEJ7UfP)(PiYKF{f8Z{E7p-aTgN>*FbgKaO(99Z@+Qa3=U;
zRr>d}`)+Rfl6pj{xJ|Ce$#T)EfZN++>Mg!}?F;*s;xD7y^4IOiseNW&WyNhAXDm0I
z&>=cwS@4(T4n3HO-Vc-1{Z$S*{aTQ>kXx
z(Y*#okL~(b?3H(!XYK-)J4;*gZme{xaC-1*j+n*K&({o2I|u!;U!ZUz*m<9wYl-aX
zURh(KrUR~RuPZJvC+V(MA|t}`;pvp@(&@8D%vNUI`L%voI>UKxgY}=ntph7h
zmEC=MFDr@T@Jyw1;Wy*Uwk)WOzpCB!tB9>%=d(Qb+Ssef42erlhkZzM>5>f&XaxBuY6iy|Lv;^9e2+&K8?8W
zUOA_w%${A~YboD%;jdrWFMlzPPknLIutdR#p(Ob*uR{uheqYZ-Tdo-6PrBJGcBX&7
z&6;+<>iYi;r|x}re*dfe2Iq1O=ihHyE}!f_dG$%L?wkE$cA+0y4&^Ut>f_M*&lKRz
z&Y`_p*FgiAaX=X`z?+dtgc*@Jk>x;{69%?4f>>0^ssY}tY#_rJfv^}z&jvb$fdK&Y
ClE#Vv
diff --git a/rest_api_odoo/__manifest__.py b/rest_api_odoo/__manifest__.py
index 76914b0e7..694df2e04 100644
--- a/rest_api_odoo/__manifest__.py
+++ b/rest_api_odoo/__manifest__.py
@@ -33,7 +33,7 @@
"depends": ['base', 'web'],
"data": [
'security/ir.model.access.csv',
- 'views/res_users_views.xml',
+ 'security/res_api_odoo_security.xml',
'views/connection_api_views.xml'
],
'images': ['static/description/banner.png'],
diff --git a/rest_api_odoo/controllers/main.py b/rest_api_odoo/controllers/main.py
index 0f02df40b..f5892d604 100644
--- a/rest_api_odoo/controllers/main.py
+++ b/rest_api_odoo/controllers/main.py
@@ -24,27 +24,43 @@ import json
import logging
from datetime import datetime
-from odoo import http
+from odoo import http, models
from odoo.http import request, Response
from ast import literal_eval
_logger = logging.getLogger(__name__)
-class RestApi(http.Controller):
- """This is a controller which is used to generate responses based on the
- api requests"""
+class IrHttp(models.AbstractModel):
+ """This model is used to authenticate the api-key when sending a request"""
+ _inherit = "ir.http"
- def auth_api_key(self, api_key):
+ @classmethod
+ def _auth_method_rest_api(cls):
"""This function is used to authenticate the api-key when sending a
request"""
- user_id = request.env["res.users"].search([("api_key", "=", api_key)])
- if api_key is not None and user_id:
- return Response(json.dumps({"message": "Authorized"}), status=200)
- elif not user_id:
- return Response(json.dumps({"message": "Invalid API Key"}), status=401)
- return Response(json.dumps({"message": "No API Key Provided"}), status=400)
+ try:
+ api_key = request.httprequest.headers.get("Authorization").lstrip("Bearer ")
+ if not api_key:
+ raise PermissionError("Invalid API key provided.")
+
+ user_id = request.env["res.users.apikeys"]._check_credentials(
+ scope="rpc", key=api_key
+ )
+ request.update_env(user_id)
+ except Exception as e:
+ _logger.error(e)
+ return Response(
+ json.dumps({"message": e.args[0]}),
+ status=401,
+ content_type="application/json",
+ )
+
+
+class RestApi(http.Controller):
+ """This is a controller which is used to generate responses based on the
+ api requests"""
def simplest_type(self, input):
"""Try cast input into native Python class, otherwise return as string"""
@@ -123,7 +139,8 @@ class RestApi(http.Controller):
)
return Response(
- json.dumps({"records": self.sanitize_records(partner_records)})
+ json.dumps({"records": self.sanitize_records(partner_records)}),
+ content_type="application/json",
)
if method == "POST":
if not option.is_post:
@@ -139,6 +156,7 @@ class RestApi(http.Controller):
return Response(
json.dumps({"new_record": self.sanitize_records(partner_records)}),
status=201,
+ content_type="application/json",
)
if method == "PUT":
if not option.is_put:
@@ -162,7 +180,8 @@ class RestApi(http.Controller):
return Response(
json.dumps(
{"updated_record": self.sanitize_records(partner_records)}
- )
+ ),
+ content_type="application/json",
)
if method == "DELETE":
if not option.is_delete:
@@ -188,12 +207,17 @@ class RestApi(http.Controller):
}
),
status=202,
+ content_type="application/json",
)
# If not using any method above, simply return an error
raise NotImplementedError()
except ValueError as e:
- return Response(json.dumps({"message": e.args[0]}), status=403)
+ return Response(
+ json.dumps({"message": e.args[0]}),
+ status=403,
+ content_type="application/json",
+ )
except NotImplementedError as e:
return Response(
json.dumps(
@@ -202,16 +226,20 @@ class RestApi(http.Controller):
}
),
status=405,
+ content_type="application/json",
)
- except Exception:
+ except Exception as e:
+ _logger.error(e)
return Response(
- json.dumps({"message": "Internal server error"}), status=500
+ json.dumps({"message": f"Internal server error. {e.args[0]}"}),
+ status=500,
+ content_type="application/json",
)
@http.route(
["/send_request"],
type="http",
- auth="none",
+ auth="rest_api",
methods=["GET", "POST", "PUT", "DELETE"],
csrf=False,
)
@@ -221,12 +249,7 @@ class RestApi(http.Controller):
generate the result"""
http_method = request.httprequest.method
- api_key = request.httprequest.headers.get("api-key")
- auth_api = self.auth_api_key(api_key)
model = kw.pop("model")
- username = request.httprequest.headers.get("login")
- password = request.httprequest.headers.get("password")
- request.session.authenticate(request.session.db, username, password)
model_id = request.env["ir.model"].search([("model", "=", model)])
if not model_id:
return Response(
@@ -236,34 +259,7 @@ class RestApi(http.Controller):
}
),
status=403,
+ content_type="application/json",
)
- if auth_api.status_code == 200:
- result = self.generate_response(http_method, model=model_id.id, **kw)
- return result
- else:
- return auth_api
-
- @http.route(
- ["/odoo_connect"], type="http", auth="none", csrf=False, methods=["GET"]
- )
- def odoo_connect(self, **kw):
- """This is the controller which initializes the api transaction by
- generating the api-key for specific user and database"""
-
- username = request.httprequest.headers.get("login")
- password = request.httprequest.headers.get("password")
- db = request.httprequest.headers.get("db")
- try:
- request.session.update(http.get_default_session(), db=db)
- auth = request.session.authenticate(request.session.db, username, password)
- user = request.env["res.users"].browse(auth)
- api_key = request.env.user.generate_api(username)
- datas = json.dumps(
- {"Status": "auth successful", "User": user.name, "api-key": api_key}
- )
- return Response(datas)
- except Exception:
- return Response(
- json.dumps({"message": "wrong login credentials"}), status=401
- )
+ return self.generate_response(http_method, model=model_id.id, **kw)
diff --git a/rest_api_odoo/models/__init__.py b/rest_api_odoo/models/__init__.py
index e9bf22051..27f8df7c3 100644
--- a/rest_api_odoo/models/__init__.py
+++ b/rest_api_odoo/models/__init__.py
@@ -21,4 +21,3 @@
#############################################################################
from . import connection_api
-from . import res_users
diff --git a/rest_api_odoo/models/res_users.py b/rest_api_odoo/models/res_users.py
deleted file mode 100644
index a2d861f36..000000000
--- a/rest_api_odoo/models/res_users.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# -*- coding:utf-8 -*-
-#############################################################################
-#
-# Cybrosys Technologies Pvt. Ltd.
-#
-# Copyright (C) 2023-TODAY Cybrosys Technologies()
-# Author: Cybrosys Techno Solutions()
-#
-# You can modify it under the terms of the GNU LESSER
-# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
-#
-# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
-# (LGPL v3) along with this program.
-# If not, see .
-#
-#############################################################################
-
-import uuid
-from odoo import fields, models
-
-
-class UserLogin(models.Model):
- """This class is used to inherit users and add api key generation"""
- _inherit = 'res.users'
-
- api_key = fields.Char(string="API Key", readonly=True,
- help="Api key for connecting with the "
- "Database.The key will be "
- "generated when authenticating "
- "rest api.")
-
- def generate_api(self, username):
- """This function is used to generate api-key for each user"""
- users = self.env['res.users'].sudo().search([('login', '=', username)])
- if not users.api_key:
- users.api_key = str(uuid.uuid4())
- key = users.api_key
- else:
- key = users.api_key
- return key
diff --git a/rest_api_odoo/security/res_api_odoo_security.xml b/rest_api_odoo/security/res_api_odoo_security.xml
new file mode 100644
index 000000000..545fd5132
--- /dev/null
+++ b/rest_api_odoo/security/res_api_odoo_security.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ REST API
+ Helps you manage your REST API records.
+ 17
+
+
+
+ Manager
+
+
+
+
+
+
+
+
+
+ All REST APIs
+
+ [(1,'=',1)]
+
+
+
+
+
\ No newline at end of file
diff --git a/rest_api_odoo/static/description/assets/screenshots/screenshot_2.png b/rest_api_odoo/static/description/assets/screenshots/screenshot_2.png
index c892dd0686b954d9364d50da43ff333bb2483bc0..94edbd0e01b839ccbd3624e71359ad0e6b5bce94 100644
GIT binary patch
literal 118405
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