-
-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(Shop import): new script to upload prices from a shop csv #574
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# Uploading shop price data | ||
|
||
## Context | ||
|
||
One of our data sources is shop imports (supermarkets uploading data directly). | ||
|
||
Currently, some members are active volunteers in food co-ops, and get authorization to extract and upload data from their shops. | ||
|
||
## Usage | ||
|
||
### Step 0: prerequisites | ||
|
||
* have a .csv file of the prices | ||
* upload these prices to a dedicated shop account please! See other shops for examples: `elefan-grenoble`, `400coop-paris11` | ||
|
||
### Step 1: get your API token from Open Prices | ||
|
||
https://prices.openfoodfacts.org/api/docs#/auth/auth_create | ||
|
||
### Step 2: upload a proof | ||
|
||
Use the token returned in Step 1. | ||
|
||
You can upload your proof via Postman (change the key to "File"). | ||
|
||
### Step 3: get your file ready | ||
|
||
The file must be a `.csv`. | ||
|
||
### Step 4: upload your file | ||
|
||
#### Upload command | ||
|
||
Use the token returned in Step 1. | ||
|
||
``` | ||
FILEPATH=../data/Elefan/20241208_articles_actif.csv PRODUCT_CODE_FIELD=Code PRODUCT_NAME_FIELD=Designation PRICE_FIELD="Prix Vente (€)" CURRENCY=EUR LOCATION_OSM_ID=1392117416 LOCATION_OSM_TYPE=NODE DATE=2024-12-08 PROOF_ID=1234 API_ENDPOINT=https://prices.openfoodfacts.net/api/v1 API_TOKEN=username_token-hash poetry run python scripts/shop_import/create_prices_from_csv.py | ||
``` | ||
|
||
Last changes when you're ready: | ||
- replace the API_ENDPOINT with `https://prices.openfoodfacts.org/api/v1` | ||
- `DRY_RUN=False` to actually upload your data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import os | ||
import sys | ||
import time | ||
|
||
import openfoodfacts | ||
|
||
from scripts.utils import create_price, read_csv | ||
|
||
OPEN_PRICES_CREATE_PRICE_ENDPOINT = f'{os.environ.get("API_ENDPOINT")}/prices' | ||
OPEN_PRICES_TOKEN = os.environ.get("API_TOKEN") | ||
|
||
REQUIRED_ENV_PARAMS = [ | ||
# "FILEPATH" | ||
# "DELIMITER" (optional) | ||
"PRODUCT_CODE_FIELD", | ||
"PRODUCT_NAME_FIELD", | ||
"PRICE_FIELD", | ||
"CURRENCY", | ||
"LOCATION_OSM_ID", | ||
"LOCATION_OSM_TYPE", | ||
"DATE", | ||
"PROOF_ID", | ||
"API_ENDPOINT", | ||
"API_TOKEN", | ||
# DRY_RUN | ||
] | ||
|
||
|
||
def map_gdpr_price_list_to_open_prices(price_list, extra_data={}): | ||
# map source fields to op fields | ||
open_prices_price_list = list() | ||
for price in price_list: | ||
open_prices_price = dict() | ||
# product_name | ||
if os.environ.get("PRODUCT_NAME_FIELD"): | ||
open_prices_price["product_name"] = price.get( | ||
os.environ.get("PRODUCT_NAME_FIELD") | ||
) | ||
# product_code | ||
open_prices_price["product_code"] = price.get( | ||
os.environ.get("PRODUCT_CODE_FIELD") | ||
) | ||
# price | ||
price_str = price.get(os.environ.get("PRICE_FIELD")) | ||
open_prices_price["price"] = ( | ||
float(price_str.replace(",", ".")) if price_str else None | ||
) | ||
# print(open_prices_price) | ||
open_prices_price_list.append({**open_prices_price, **extra_data}) | ||
|
||
return open_prices_price_list | ||
|
||
|
||
def filter_rules(op_price_list): | ||
""" | ||
Rules to skip some prices (on code, name...) | ||
""" | ||
op_price_list_filtered = list() | ||
|
||
for op_price in op_price_list: | ||
passes_test = True | ||
|
||
if not op_price["product_code"]: | ||
passes_test = False | ||
elif not op_price["product_code"].isnumeric(): | ||
passes_test = False | ||
elif len(op_price["product_code"]) < 6: | ||
passes_test = False | ||
elif not openfoodfacts.barcode.has_valid_check_digit(op_price["product_code"]): | ||
passes_test = False | ||
|
||
if not op_price["price"]: | ||
passes_test = False | ||
|
||
if passes_test: | ||
op_price_list_filtered.append(op_price) | ||
|
||
return op_price_list_filtered | ||
|
||
|
||
if __name__ == "__main__": | ||
""" | ||
How-to run: | ||
> FILEPATH= poetry run python scripts/shop_import/create_prices_from_csv.py | ||
Required params: see REQUIRED_ENV_PARAMS | ||
""" | ||
# Step 1: read input file | ||
if not os.environ.get("FILEPATH"): | ||
sys.exit("Error: missing FILEPATH env") | ||
filepath = os.environ.get("FILEPATH") | ||
print(f"===== Reading {filepath}") | ||
if os.environ.get("DELIMITER"): | ||
price_list = read_csv(filepath, delimiter=os.environ.get("DELIMITER")) | ||
else: | ||
price_list = read_csv(filepath) | ||
print(len(price_list)) | ||
|
||
print("===== Input example:") | ||
print(price_list[0]) | ||
|
||
# Step 2: check env params are all present | ||
print("===== Checking env params") | ||
for env_param in REQUIRED_ENV_PARAMS: | ||
if not os.environ.get(env_param): | ||
sys.exit(f"Error: missing {env_param} env") | ||
print("All good :)") | ||
|
||
# Step 3: transform input into OP format | ||
print("===== Mapping source file to Open Prices format") | ||
source = os.environ.get("SOURCE") | ||
extra_data = { | ||
"currency": os.environ.get("CURRENCY"), | ||
"location_osm_id": int(os.environ.get("LOCATION_OSM_ID")), | ||
"location_osm_type": os.environ.get("LOCATION_OSM_TYPE"), | ||
"date": os.environ.get("DATE"), | ||
# proof_id must be of type "SHOP_IMPORT" :) | ||
"proof_id": int(os.environ.get("PROOF_ID")), | ||
} | ||
open_prices_price_list = map_gdpr_price_list_to_open_prices( | ||
price_list, extra_data=extra_data | ||
) | ||
print(len(open_prices_price_list)) | ||
|
||
# Step 4: filter prices depending on specific rules | ||
print("===== Applying source filtering rules") | ||
open_prices_price_list_filtered = filter_rules(open_prices_price_list) | ||
print(len(open_prices_price_list_filtered)) | ||
|
||
print("===== Output example (extra fields will be ignored)") | ||
print(open_prices_price_list_filtered[0]) | ||
|
||
# Step 5: send prices to backend via API | ||
if os.environ.get("DRY_RUN") == "False": | ||
print(f"===== Uploading data to {os.environ.get('API_ENDPOINT')}") | ||
progress = 0 | ||
for index, price in enumerate(open_prices_price_list_filtered): | ||
create_price( | ||
price, os.environ.get("API_ENDPOINT"), os.environ.get("API_TOKEN") | ||
) | ||
# some pauses to be safe | ||
progress += 1 | ||
if (progress % 10) == 0: | ||
time.sleep(1) | ||
if (progress % 50) == 0: | ||
print(f"{progress}/{len(open_prices_price_list_filtered)}...") | ||
else: | ||
sys.exit("===== No prices uploaded (DRY_RUN env missing or set to 'True')") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import csv | ||
|
||
import requests | ||
|
||
|
||
def read_csv(filepath, delimiter=","): | ||
with open(filepath, newline="") as csvfile: | ||
reader = csv.DictReader(csvfile, delimiter=delimiter) | ||
return list(reader) | ||
|
||
|
||
def create_price(price, API_ENDPOINT, API_TOKEN): | ||
OPEN_PRICES_CREATE_PRICE_ENDPOINT = f"{API_ENDPOINT}/prices" | ||
headers = {"Authorization": f"Bearer {API_TOKEN}"} | ||
response = requests.post( | ||
OPEN_PRICES_CREATE_PRICE_ENDPOINT, json=price, headers=headers | ||
) | ||
if not response.status_code == 201: | ||
print(response.json()) | ||
print(price) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks to openfoodfacts/openfoodfacts-python@6faadc1