o
    £àqjgà  ã                   @   sÖ  d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddlmZ ddlmZ ddlmZmZ ddlmZ ddlmZ ddlZddlZddlZddlmZmZ dd	lm Z  dd
l!m"Z"m#Z#m$Z$ ddl%m&Z&m'Z' ddl(m)Z) ddl*m+Z+m,Z, e ƒ  ddl-m.Z. ddl/m0Z0 ddl1m2Z2m3Z3m4Z4 ddl5m6Z6 ddl7m8Z8m9Z9m:Z:m;Z;m<Z< ddl!m#Z#m$Z$m=Z=m>Z>m?Z? ddl@mAZA ejB Cdd¡ZDejB Cdd¡ZEejB Cdd¡ZFejB Cdd¡ZGejB Cdd¡ZHeIejB Cdd¡ƒZJeIejB Cd d!¡ƒZKeIejB Cd"d#¡ƒZLeIejB Cd$d¡ƒZMd%eNfd&d'„ZOd(ePd%dfd)d*„ZQejB Cd+d,¡ R¡  S¡ ZTejB Cd-d.¡ZUejB Cd/d0¡ Vd1¡ZWejB Cd2d3¡ZXejB Cd4d5¡ Y¡ ZZeIejB Cd6d7¡ƒZ[eIejB Cd8d9¡ƒZ\eeXƒj]j^d:d:d;� e _d<¡Z`e` aeZ¡ d=e`_bejcd>d?d@�Zde`je�s¤eeXe[e\dAdB�Zfef ged¡ e` hef¡ e i¡ Zjej ged¡ e` hej¡ ejB CdCdD¡ZkejB CdEdF¡ZlejB CdGd¡ZmeIejB CdHdI¡ƒZnekekeldJœZodKePd%ePfdLdM„ZpdNdOgZqe#dPdQdR�ZreTdSk�rôeeUƒj^d:d:d;� erjsdTe)eUdU�dVdW� ddXl!m?Z? dYdZ„ ejB Cd[d¡ td\¡D ƒZud]e?d%dfd^d_„ZvejB Cd`d¡ZweAdad=db�Zxe=exƒfdcePd%dfddde„ZyG dfdg„ dge+ƒZzG dhdi„ die+ƒZ{djePd%ePfdkdl„Z|dme6d%e9fdndo„Z}dpdq„ Z~dred%ejfdsdt„Z€dredued%e�e fdvdw„Z‚dxe�e dyed%efdzd{„Zƒdredued|ePd%e�e fd}d~„Z„dePd%ePfd€d�„Z…dePd%ePfd‚dƒ„Z†dePd%e‡ePePePePf fd„d…„ZˆdePd†ed%dfd‡dˆ„Z‰d‰edŠePd‹ePdŒePd%ePf
d�dŽ„ZŠd‰ed‹ePd%ePfd�d�„Z‹d‘ePd’eŒd%dfd“d”„Z�dePdKePd•ePd–ePd—ePd%dfd˜d™„ZŽdePdKePd•ePd–ePdšePd%dfd›dœ„Z�erj�d�e{e>eyƒe>evƒgdž�dŸezd e"d]e?fd¡d¢„ƒZ‘erj�d£e{e>eyƒe>evƒgdž�dŸezd e"d]e?fd¤d¥„ƒZ’erj�d¦e>eyƒe>evƒgd§�d°de,d]e?d—eeP fd¨d©„ƒZ“erj�dªe>eyƒe>evƒgd§�d°de,d]e?dšeeP fd«d¬„ƒZ”er Cd­¡d®d¯„ ƒZ•dS )±u»  
CD Report Generator API
========================
Single-file FastAPI service that:
  1. Downloads a CD-entries CSV from a given URL
  2. Generates one styled landscape-A3 PDF per drug (register name group)
  3. Zips all the PDFs together
  4. Uploads the zip to a target S3 "report_files/" location (built from a
     template in .env, using values parsed out of the CSV URL)
  5. Returns immediately, then POSTs the result to the correct Laravel
     endpoint (stage or live) once background processing finishes.

No database, no ORM models â€” everything happens in-memory / temp-dir per request.

-----------------------------------------------------------------------------
CONFIG (env vars)
-----------------------------------------------------------------------------
AWS_ACCESS_KEY_ID       - AWS access key (or use an attached IAM role / profile)
AWS_SECRET_ACCESS_KEY   - AWS secret key
AWS_REGION              - e.g. "eu-west-2"           (default: eu-west-2)
S3_BUCKET               - bucket name that backs the pharm.amazonaws.com domain
S3_PUBLIC_BASE_URL      - optional. Public base URL to build the returned link,
                           e.g. "https://pharm.amazonaws.com". If not set, a
                           presigned URL is generated instead.
ZIP_KEY_TEMPLATE        - template used to build the destination S3 key for
                           the zip, using placeholders parsed from csv_url:
                             {receive_form} - "local" / "stage" / "live" segment
                             {pharmacy_id}  - the folder segment right before
                                              "report_files" in csv_url
                             {report_stem}  - the CSV filename without ".csv"
                           Default:
                             "{receive_form}/pharmacies/{pharmacy_id}/report_files/{report_stem}.zip"
STORAGE_BACKEND         - "s3" (default) or "local".
                           Set to "local" for local development/testing with
                           NO AWS account or credentials needed at all â€”
                           /generate-report will save the zip to
                           LOCAL_OUTPUT_DIR on disk instead of uploading to S3.
LOCAL_OUTPUT_DIR        - folder to save zips into when STORAGE_BACKEND=local
                           (default: "./local_zip_output")
MAX_ROWS_PER_PDF        - max rows rendered into ONE ReportLab document before
                           generate_pdfs()/generate_single_pdf() split off
                           another "_PartN" PDF. Exists to bound peak memory
                           on huge CSVs â€” see the comment above the constant
                           for the full explanation. Default: 20000

-----------------------------------------------------------------------------
CALLBACK ROUTING (Laravel)
-----------------------------------------------------------------------------
csv_url's first path segment tells us which environment the request came
from ("local", "stage", or "live"), which decides which Laravel endpoint
gets the completion callback:

    csv_url segment    -> callback goes to
    ----------------------------------------
    local              -> STAGE_LARAVEL_URL   (no separate local Laravel API)
    stage              -> STAGE_LARAVEL_URL
    live               -> LIVE_LARAVEL_URL

STAGE_LARAVEL_URL  - default "https://devstage.pharmsmart.co.uk/api/ext/v2/pharmacy/aws-csv-pdf-converter/index.php"
LIVE_LARAVEL_URL   - default "https://pharmsmart.co.uk/api/ext/v2/pharmacy/aws-csv-pdf-converter/index.php"
LARAVEL_SECRET_KEY - shared secret used to HMAC-sign the callback (Bearer token
                      sent is HMAC-SHA256(pharmacy_id|csv_filename), hex digest).
CALLBACK_TIMEOUT_SECONDS - request timeout for the callback POST (default: 30)

-----------------------------------------------------------------------------
LOGGING
-----------------------------------------------------------------------------
Every call to /generate-report, /generate-csv-to-pdf-report, and their
/download variants (request received, validation result, background job
progress, storage result, callback attempt/response, and any errors) is
written to a rotating log file, in addition to stdout (so it still shows up
under uvicorn/systemd/docker logs as before).

LOG_FILE_PATH   - path to the log file (default: "logs/api.log"). Parent
                   directories are created automatically if missing.
LOG_LEVEL       - default "INFO"
LOG_MAX_BYTES   - size in bytes before the log file rotates (default: 5MB)
LOG_BACKUP_COUNT- number of rotated backups to keep (default: 5)

-----------------------------------------------------------------------------
RUN
-----------------------------------------------------------------------------
pip install fastapi uvicorn pandas reportlab boto3 requests python-multipart python-dotenv
Create a ".env" file in the same directory (see .env.example) then run:
uvicorn main:app --host 0.0.0.0 --port 8000

-----------------------------------------------------------------------------
USAGE
-----------------------------------------------------------------------------
POST /generate-report
{
  "csv_url": "https://pharmsmart-pharmacy.s3.eu-west-2.amazonaws.com/local/pharmacies/N0tZbTJJRTFSYVhHanpGSm1EbVFKdz09/report_files/6a38ce059932a-cd-entries.csv"
}

Returns IMMEDIATELY:
{
  "status": "success"
}

The heavy CSV -> PDFs -> zip -> upload work runs in a background task. Once
it finishes, this JSON is POSTed to the Laravel endpoint chosen by the
csv_url's environment segment (see CALLBACK ROUTING above):

{
  "request_type": "csv-to-pdf",
  "timestamp": 1784021199,
  "pharmacy_id": "N0tZbTJJRTFSYVhHanpGSm1EbVFKdz09",
  "result_filename": "6a38ce059932a-cd-entries.zip",
  "csv_filename": "6a38ce059932a-cd-entries.csv"
}

with header:
  Authorization: Bearer <HMAC-SHA256(pharmacy_id|csv_filename, LARAVEL_SECRET_KEY)>

On failure, "status": "failed" and "error": "<message>" are added to that
same payload (Laravel isn't required to look at these, but they're there
for visibility / debugging).

There is also POST /generate-report/download which streams the zip straight
back in the HTTP response instead of uploading anywhere (handy for testing
without S3 credentials). This one is still synchronous.

POST /generate-csv-to-pdf-report and /generate-csv-to-pdf-report/download
behave the same way but produce a single combined PDF (or, for CSVs larger
than MAX_ROWS_PER_PDF rows, several PDF parts bundled into a zip â€” see
MAX_ROWS_PER_PDF above) instead of one PDF per drug.
é    N)Úescape)ÚRotatingFileHandler)ÚPathÚPurePosixPath)ÚOptional)Úurlparse)ÚBotoCoreErrorÚClientError)Úload_dotenv)ÚBackgroundTasksÚFastAPIÚHTTPException)ÚFileResponseÚJSONResponse)ÚStaticFiles)Ú	BaseModelÚHttpUrl)Úcolors)ÚTA_LEFT)ÚA2ÚA3Ú	landscape)ÚParagraphStyle)Ú	LongTableÚ	ParagraphÚSimpleDocTemplateÚTableÚ
TableStyle)r   r   ÚSecurityÚDependsÚRequest)ÚAPIKeyHeaderÚ
AWS_REGIONz	eu-west-2Ú	S3_BUCKETÚ ÚS3_PUBLIC_BASE_URLÚZIP_KEY_TEMPLATEzF{receive_form}/pharmacies/{pharmacy_id}/report_files/{report_stem}.zipÚPDF_KEY_TEMPLATEzF{receive_form}/pharmacies/{pharmacy_id}/report_files/{report_stem}.pdfÚPRESIGNED_URL_EXPIRY_SECONDSi  ÚMAX_ROWS_PER_PDFi N  ÚMAX_REGISTER_GROUPSi¸  ÚMAX_MEMORY_MBÚreturnc                   C   s   t  t j¡jd S )u‚   Current process peak RSS in MB. ru_maxrss is KB on Linux (bytes on
    macOS) â€” this service is assumed to run on Linux servers.i   )ÚresourceÚ	getrusageÚRUSAGE_SELFÚ	ru_maxrss© r1   r1   ú(/var/www/html/pharmsmart-cdr-gen/main.pyÚ_current_rss_mbÜ   s   r3   Úcontextc              	   C   s@   t dkrdS tƒ }|t krtdd| › d|d›dt › d�d	�‚dS )
zyRaise a controlled, catchable error if RSS has crossed MAX_MEMORY_MB.
    No-op if MAX_MEMORY_MB is 0 (default/disabled).r   Niû  z	Aborting z: process RSS ú.0fzMB exceeded MAX_MEMORY_MB=zMB safety limit.©Ústatus_codeÚdetail)r+   r3   r   )r4   Úrssr1   r1   r2   Ú_check_memory_guardâ   s   ÿýÿr:   ÚSTORAGE_BACKENDÚs3ÚLOCAL_OUTPUT_DIRz./local_zip_outputÚLOCAL_PUBLIC_BASE_URLzhttp://localhost:8000ú/ÚLOG_FILE_PATHzlogs/api.logÚ	LOG_LEVELÚINFOÚLOG_MAX_BYTESi  P ÚLOG_BACKUP_COUNTé   T©ÚparentsÚexist_okÚcd_report_apiFz+%(asctime)s | %(levelname)-8s | %(message)sz%Y-%m-%d %H:%M:%S)ÚfmtÚdatefmtúutf-8)ÚmaxBytesÚbackupCountÚencodingÚSTAGE_LARAVEL_URLzUhttps://devstage.pharmsmart.co.uk/api/ext/v2/pharmacy/aws-csv-pdf-converter/index.phpÚLIVE_LARAVEL_URLzLhttps://pharmsmart.co.uk/api/ext/v2/pharmacy/aws-csv-pdf-converter/index.phpÚLARAVEL_SECRET_KEYÚCALLBACK_TIMEOUT_SECONDSé   )ÚlocalÚstageÚliveÚreceive_formc                 C   s6   t  |  ¡ ¡}|stdd| › dd t ¡› d�d�‚|S )Né¦  zUnrecognised environment 'z' in csv_url (expected one of: z, z).r6   )ÚCALLBACK_URL_BY_ENVÚgetÚlowerr   Újoin)rX   Úurlr1   r1   r2   Úresolve_callback_url2  s   ÿþr_   ÚIDzAmended Entry IDzPharmsmart CD Report Generatorz1.0.0)ÚtitleÚversionrU   z/files)Ú	directoryÚfiles©Úname)r    c                 C   s"   g | ]}|  ¡ r|  ¡  d ¡‘qS )r?   )ÚstripÚrstrip)Ú.0Úor1   r1   r2   Ú
<listcomp>M  s    ýÿrk   ÚALLOWED_ORIGINSú,Úrequestc                    sr   t sd S | j d¡p| j d¡pd d¡‰ ˆ sd S t‡ fdd„t D ƒƒs7t dˆ › d�¡ td	d
ˆ › d�d�‚d S )NÚoriginÚrefererr$   r?   c                 3   s&   � | ]}ˆ |kpˆ   |d  ¡V  qdS )r?   N)Ú
startswith)ri   Úallowed©ro   r1   r2   Ú	<genexpr>`  s   €$ z verify_domain.<locals>.<genexpr>zBlocked request: origin 'z' not in ALLOWED_ORIGINSi“  zOrigin 'z' not allowed.r6   )rl   Úheadersr[   rh   ÚanyÚloggerÚwarningr   )rn   r1   rs   r2   Úverify_domainT  s   "þry   ÚAPI_KEYz	X-API-Key)rf   Ú
auto_errorÚapi_keyc                 C   s6   t stddd�‚| r| t krt d¡ tddd�‚d S )Néô  zServer API key not configured.r6   z.Rejected request: invalid or missing X-API-Keyi‘  zInvalid or missing API key.)rz   r   rw   rx   )r|   r1   r1   r2   Úverify_api_keyl  s   
þr~   c                   @   ó   e Zd ZU eed< dS )ÚGenerateReportRequestÚcsv_urlN)Ú__name__Ú
__module__Ú__qualname__r   Ú__annotations__r1   r1   r1   r2   r€   w  ó   
 r€   c                   @   r   )ÚGenerateReportResponseÚstatusN)r‚   rƒ   r„   Ústrr…   r1   r1   r1   r2   r‡   {  r†   r‡   rf   c                 C   s8   t | ƒ ¡ } t dd| ¡} t dd| ¡} | d d… pdS )Nz	[^\w\s.-]r$   z\s+Ú_é_   ÚUnknown_Drug)r‰   rg   ÚreÚsubre   r1   r1   r2   Úclean_filename�  s   r�   Ústylec                 C   s   t t| ƒƒ dd¡}t||ƒS )NÚ
z<br/>)r   r‰   Úreplacer   )Úvaluer�   Útextr1   r1   r2   Úmake_paragraphˆ  s   
r•   c                    sd  g }| D ]•}t |ƒ ¡  ¡ }d|v rd|v r| d¡ qd|v s$d|v r*| d¡ qd|v r4| d¡ qd|v s<d	|v rFd|vrF| d
¡ qd|v rP| d¡ qd|v rZ| d¡ qd|v sfd|v sfd|v rl| d¡ qd|v rv| d¡ qd|v r€| d¡ qd|v rŠ| d¡ qd|v r”| d¡ q| d¡ qttƒ\}}|d ‰t|ƒ‰ ‡ ‡fdd„|D ƒS )NÚregisterrf   gffffff@Ú	timestampÚdateg      ô?Úactiong333333ó?ÚaddressgÍÌÌÌÌÌ@Ú
prescribergffffff @Úcollectgffffffú?Úidrn   ÚprovidedÚquantitygq=
×£pí?Úbalancegffffffî?Úentryg      ð?ÚsigngÍÌÌÌÌÌä?gÍÌÌÌÌÌð?é   c                    s   g | ]}ˆ| ˆ  ‘qS r1   r1   ©ri   Úw©Útotal_weightÚusable_widthr1   r2   rk   °  s    z%get_column_widths.<locals>.<listcomp>)r‰   rg   r\   Úappendr   r   Úsum)ÚcolumnsÚweightsÚcÚlcÚ
page_widthrŠ   r1   r¦   r2   Úget_column_widths�  s<   r°   Úcsv_pathc                    s"  g d¢}d}|D ]}zt j| |td�W   S  ttfy+ } z|}W Y d}~qd}~w t jjy‡ } zOz5g ‰ dtt ddf‡ fdd„}t j| |dt|d	�}ˆ r_t 	d
t
ˆ ƒ› d| j› d�¡ |W W  Y d}~  S  ty‚ } z|}W Y d}~W Y d}~qd}~ww d}~ww tdd|› �d�‚)u�  Read a CSV trying multiple encodings, since pharmacy exports are often
    not UTF-8 (commonly Windows-1252 / Latin-1 from Excel/Windows systems).
    Also falls back to a more lenient parser if some rows have a ragged
    number of fields (e.g. an unescaped comma inside a text field).

    Reads every column as dtype=str: this data ends up rendered as strings
    anyway (callers already do .astype(str)), and reading it that way from
    the start avoids pandas' per-chunk numeric-type inference/conversion â€”
    which is what throws the 'Columns (...) have mixed types' DtypeWarning
    and roughly doubles peak memory during parsing on large files.)z	utf-8-sigrL   Úcp1252zlatin-1N)rO   ÚdtypeÚbad_liner,   c                    s   ˆ   tˆ ƒ¡ d S ©N)r©   Úlen)r´   ©Úbad_rowsr1   r2   Ú_log_bad_lineË  s   z-read_csv_with_fallback.<locals>._log_bad_lineÚpython)rO   Úenginer³   Úon_bad_lineszSkipped z malformed row(s) in z? (ragged field count, e.g. an unescaped comma in a text field).rY   zCould not parse CSV file: r6   )ÚpdÚread_csvr‰   ÚUnicodeDecodeErrorÚUnicodeErrorÚerrorsÚParserErrorÚlistrw   rx   r¶   rf   Ú	Exceptionr   )r±   Úencodings_to_tryÚ
last_errorrO   Úer¹   ÚdfÚe2r1   r·   r2   Úread_csv_with_fallback³  sB   €ûÿ€þ€ërÊ   Ú
output_dirc                    sÊ  t | ƒ d¡ t¡‰ˆj‡fdd„tD ƒd�‰d}ˆjD ]}d| ¡ v r.d| ¡ v r.|} nq|du r8ˆjd }tˆƒ}ˆ| j	d	d
�}t
 d|› d|› d|› dtƒ d›d�	¡ |tkr~t
 d|› d|› dt› d|› d�	¡ tdd|› d|› dt› d�d�‚tdddddd d!�}td"dd#d$dd%d&�‰td'd(d)d$d%d*�‰ tˆjƒ}t|ƒ}g }	d}
ˆj|d	d+d,�D �]!\}}t|ƒ ¡ p¼d-}t|ƒ}tdt |t ¡ƒ}|dkrát
 d.|› d|› d/|› d0t› d1�	¡ t|ƒD ]º}|t }t|t |ƒ}|j||… }|dk�rd2|d › �nd}|d3t|ƒ› |› d4� }‡fd5d„|D ƒg}| ¡ D ]\}}| ‡ fd6d„| ¡ D ƒ¡ �q!d7t |ƒ› �}|dk�rN|d8|d › d9|› d:�7 }t!t|ƒt"t#ƒddd;dd<�}t$||dd+d=�}| %t&d>d?d@t' (dA¡fdBd?d@t'j)fdCd?dDdEt'j)fdFdGdHdIdJgƒ¡ t*||ƒ|g}| +|¡ |	 |¡ ~~~~~t, -¡  qå~|
d7 }
|
dK dk�s±|
|k�rÇt
 dL|
› dM|› dNt|	ƒ› dOtƒ d›d�	¡ t.dP|
› dM|› d:�ƒ q°t
 dQt|	ƒ› dRtˆƒ› dS�¡ |	S )Tu  Generate one drug-wise styled PDF per group, returns list of PDF paths.

    Any register-name group larger than MAX_ROWS_PER_PDF rows is split into
    multiple "_PartN" PDFs, built and flushed to disk one at a time. This is
    what keeps peak memory bounded on very large CSVs (previously, a single
    huge group meant building millions of ReportLab Paragraph objects in
    memory at once before doc.build() ran â€” enough to get the whole worker
    process OOM-killed silently, with no exception ever logged).r$   c                    s   g | ]	}|ˆ j v r|‘qS r1   ©r«   )ri   Úcol)rÈ   r1   r2   rk   í  s    z!generate_pdfs.<locals>.<listcomp>rÌ   Nr–   rf   r   F)Údropnaz[generate_pdfs] CSV has z row(s), grouping by column 'z' -> z unique group(s) | RSS=r5   ÚMBz[generate_pdfs] Column 'z' has z. unique values, exceeding MAX_REGISTER_GROUPS=uð   . This almost always means the wrong column was picked as the drug/register-name grouping column (it should have low cardinality â€” a handful to a few hundred distinct drug names, not thousands of near-unique values). Refusing to generate u2    separate PDFs â€” check the CSV's column headers.rY   zColumn 'z& unique values (> MAX_REGISTER_GROUPS=zy); refusing to generate that many per-drug PDFs. Check that the CSV's register/drug-name column was identified correctly.r6   ÚReportTitlez
Times-Boldé   é   é   é   )ÚfontNameÚfontSizeÚleadingÚ	alignmentÚ
spaceAfterÚHeadergÍÌÌÌÌÌ@gffffff @ÚCJK©rÕ   rÖ   r×   rØ   ÚwordWrapÚBodyzTimes-Romang      @)rÕ   rÖ   r×   rÝ   T)rÎ   ÚsortzUnknown Drugz[generate_pdfs] Register 'ú rows -> splitting into ú PDF part(s) of up to ú
 rows eachÚ_PartÚ
CD_Report_ú.pdfc                    ó   g | ]}t |ˆ ƒ‘qS r1   ©r•   ©ri   r­   ©Úheader_styler1   r2   rk   A  ó    c                    ræ   r1   rç   )ri   r“   ©Ú
body_styler1   r2   rk   C  rë   zCD Reports - z (Part z of ú)é   ©ÚpagesizeÚ
leftMarginÚrightMarginÚ	topMarginÚbottomMargin)Ú	colWidthsÚ
repeatRowsÚ
splitByRowÚ
BACKGROUND©r   r   ©éÿÿÿÿr   z#D0D0D0Ú	TEXTCOLORÚGRID©rü   rü   gÍÌÌÌÌÌÜ?©ÚVALIGNrú   rÿ   ÚTOP©ÚLEFTPADDINGrú   rÿ   é   ©ÚRIGHTPADDINGrú   rÿ   r  )Ú
TOPPADDINGrú   rÿ   é   )ÚBOTTOMPADDINGrú   rÿ   r	  éd   z[generate_pdfs] Progress: r?   z group(s), z PDF(s) so far | RSS=zgenerate_pdfs (after group z[generate_pdfs] Done: ú PDF file(s) from ú total row(s))/rÊ   ÚfillnaÚastyper‰   ÚdropÚEXCLUDE_COLUMNSr«   r\   r¶   Únuniquerw   Úinfor3   r*   Úerrorr   r   rÃ   r°   Úgroupbyrg   ÚmaxÚmathÚceilr)   ÚrangeÚminÚilocr�   Úiterrowsr©   Útolistr   r   r   r   r   ÚsetStyler   r   ÚHexColorÚblackr   ÚbuildÚgcrœ   r:   )r±   rË   Úregister_colrÍ   Útotal_rows_allÚunique_groupsÚtitle_styler«   Ú
col_widthsÚ	pdf_pathsÚgroups_processedÚ	drug_nameÚgroupÚ
drug_labelÚ
total_rowsÚ	num_partsÚpart_idxÚstartÚendÚchunkÚsuffixÚpdf_pathÚ
table_datarŠ   ÚrowÚ
title_textÚdocÚtableÚstoryr1   )rí   rÈ   rê   r2   Úgenerate_pdfsâ  s  	
€
ÿÿÿÿÿüÿÿý
úúû
ÿÿÿ 
ú	üøÿÿ



ÿÿÿ r;  r(  Úzip_pathc                 C   sP   t  |dt j¡�}| D ]
}|j||jd� qW d   ƒ |S 1 s!w   Y  |S )Nr¥   )Úarcname)ÚzipfileÚZipFileÚZIP_DEFLATEDÚwriterf   )r(  r<  ÚzipfÚpdfr1   r1   r2   Úzip_pdfs  s   ÿ
ÿýrD  Úbase_filenamec                    sÜ  t | ƒ d¡ t¡}t|jƒ}|dkrttƒnttƒ}|d }d}d}|| | ‰t	ddddt
d	d
�‰ t	dˆ dd�‰g }	|jD ] }
tt|
ƒt|ƒrR||
 j ¡  ¡ nt|
ƒƒ}|	 t|dƒ¡ q?t|	ƒped‰‡‡fdd„|	D ƒ}t|ƒ}|ˆkr…ˆ| ‰‡fdd„|D ƒ}t|jƒ}t|ƒ}tdt |t ¡ƒ}|dkr«t d|› d|› dt› d�¡ g }t|ƒD ]¬}|t }t|t |ƒ}|j||… }|dkrÐd|d › �nd}||› |› d� }‡fdd„|D ƒg}| ¡ D ]\}}| ‡ fdd„|D ƒ¡ qét|d|dd�}| tdddt d ¡fd!dd"d#tjfd$d%d&d'd(d)gƒ¡ tt|ƒ|||ddd*�}| |g¡ | |¡ ~~~~t  !¡  t d+|d › d,|› d-t"ƒ d.›d/�¡ t#d0|d › d,|› d1�ƒ q±t d2t|ƒ› d3|› d4�¡ |S )5ur  Convert an entire CSV into one or more compact landscape PDFs (no
    drug/register grouping â€” that's what generate_pdfs() does for
    /generate-report). Used by /generate-csv-to-pdf-report.

    Returns a list of PDF paths: a single-element list for CSVs up to
    MAX_ROWS_PER_PDF rows, or multiple "{base_filename}_PartN.pdf" files for
    larger ones â€” callers should zip the result together when more than one
    path comes back. Splitting (and freeing each chunk's memory before
    starting the next) is what keeps peak RAM bounded on huge CSVs instead
    of holding millions of ReportLab Paragraph objects in memory at once,
    which is what silently OOM-kills the worker process on very large files.

    Page size auto-scales from A3 to A2 for wide CSVs, and column widths are
    sized from actual content length so text wraps sensibly instead of
    overflowing.r$   é
   r   é   ÚSinglePdfBodyÚ	HelveticarE   é   rÛ   rÜ   ÚSinglePdfHeaderúHelvetica-Bold)ÚparentrÕ   é(   rÓ   c                    s   g | ]}t d ˆ| ˆ  ƒ‘qS )é#   )r  )ri   Úl)Ú	total_lenr¨   r1   r2   rk   µ  s    z'generate_single_pdf.<locals>.<listcomp>c                    s   g | ]}|ˆ  ‘qS r1   r1   r¤   )Úscaler1   r2   rk   ¹  s    z[generate_single_pdf] rà   rá   râ   rã   rå   c                    ræ   r1   rç   rè   ré   r1   r2   rk   Ï  rë   c                    ræ   r1   rç   )ri   Úvrì   r1   r2   rk   Ñ  rë   T)r÷   rö   rø   rù   rú   rû   z#D9EAF7rþ   rÿ   g      Ð?)ÚFONTNAMErú   rû   rL  r   r  r  )r  rú   rÿ   rÓ   )r
  rú   rÿ   rÓ   rð   z%[generate_single_pdf] Progress: part r?   z done | RSS=r5   rÏ   z generate_single_pdf (after part rî   z[generate_single_pdf] Done: r  r  )$rÊ   r  r  r‰   r¶   r«   r   r   r   r   r   r  r©   r  rª   rÃ   r  r  r)   rw   r  r  r  r  r   r  r   r   r  Úgreyr   r!  r"  rœ   r3   r:   )r±   rË   rE  rÈ   Únum_colsÚ	page_sizer¯   Úleft_marginÚright_marginÚlengthsrÍ   Úmax_lenr'  Útotal_widthr«   r-  r.  r(  r/  r0  r1  r2  r3  r4  r5  rŠ   r6  r9  r8  r1   )rí   rê   rR  rQ  r¨   r2   Úgenerate_single_pdf†  s¦   
úý
,
ÿÿøÿÿú
ÿÿr]  r�   c                 C   óz   t t| ƒjƒj}t|ƒdk rtddd�‚|d }|d dks$|d d	kr*tdd
d�‚|d }t |d ƒj}tj|||d�S )z¹
    Extract:
        receive_form : local/stage/live
        pharmacy_id
        report_stem

    Example:
    https://.../local/pharmacies/<pharmacy_id>/report_files/<report>.csv
    rJ  rY   úInvalid csv_url format.r6   rÓ   r  Ú
pharmaciesé   Úreport_filesúUnexpected csv_url structure.r	  rE   ©rX   Úpharmacy_idÚreport_stem)	r   r   ÚpathÚpartsr¶   r   Ústemr&   Úformat©r�   rh  rX   re  rf  r1   r1   r2   Úbuild_zip_key_from_csv_urlÿ  s   ýrl  c                 C   r^  )u�   Same as build_zip_key_from_csv_url but for the single-PDF endpoint â€”
    stores the PDF alongside the CSV in the same report_files/ folder.rJ  rY   r_  r6   rÓ   r  r`  ra  rb  rc  r	  rE   rd  )	r   r   rg  rh  r¶   r   ri  r'   rj  rk  r1   r1   r2   Úbuild_pdf_key_from_csv_url!  s   ýrm  c                 C   sŒ   t t| ƒjƒj}t|ƒdk s|d dks|d dkr tddd�‚|d	 }|d
 }tj t| ƒj¡}tj |¡d › d�}t	|ƒ ||||fS )al  Cheap, synchronous parse of csv_url ->
    (receive_form, pharmacy_id, csv_filename, zip_filename).

    receive_form is "local" / "stage" / "live" and decides which Laravel
    endpoint the completion callback goes to. Used so /generate-report can
    validate + build the response instantly, before the actual heavy work is
    handed off to the background task.rJ  r  r`  ra  rb  rY   rc  r6   rÓ   r	  r   ú.zip)
r   r   rg  rh  r¶   r   ÚosÚbasenameÚsplitextr_   )r�   rh  rX   re  Úcsv_filenameÚzip_filenamer1   r1   r2   Úparse_csv_url_parts8  s   $rt  Ú	dest_pathc              
   C   sÈ   zMt j| ddd��:}| ¡  t|dƒ�}|jdd�D ]}| |¡ qW d   ƒ n1 s,w   Y  W d   ƒ W d S W d   ƒ W d S 1 sFw   Y  W d S  t jyc } z	tdd|› �d	�‚d }~ww )
NTé<   )ÚstreamÚtimeoutÚwbi   )Ú
chunk_sizei�  zFailed to download CSV: r6   )Úrequestsr[   Úraise_for_statusÚopenÚiter_contentrA  ÚRequestExceptionr   )r�   ru  ÚrÚfr2  rÇ   r1   r1   r2   Údownload_csvQ  s   ÿÿþ&þ€ÿr‚  Úlocal_zip_pathÚbucketÚkeyÚkey_flagc              
   C   s  |st ddd�‚tjdtd�}z/|dkr"|jt| ƒ||ddid	� n|d
kr4|jt| ƒ||ddid	� n	| t| ƒ||¡ W n ttfyX } zt dd|› d|› �d�‚d }~ww trht 	d¡› d| 
d¡› �S z|jd||dœtd�W S  ttfy‹ } z	t dd|› �d�‚d }~ww )Nr}   zONo S3 bucket configured. Set S3_BUCKET env var or pass 'bucket' in the request.r6   r<   )Úregion_nameÚzip_fileÚContentTypeúapplication/zip)Ú	ExtraArgsÚpdf_fileúapplication/pdfzFailed to upload z to S3: r?   Ú
get_object)ÚBucketÚKey)ÚParamsÚ	ExpiresInz(Zip uploaded but failed to build a URL: )r   Úboto3Úclientr"   Úupload_filer‰   r   r	   r%   rh   ÚlstripÚgenerate_presigned_urlr(   )rƒ  r„  r…  r†  r<   rÇ   r1   r1   r2   Úupload_to_s3\  s:   þ€€ÿý€ÿr˜  c              
   C   sl   t tƒ| }|jjddd� zt | |¡ W n ty+ } z	tdd|› �d�‚d}~ww t› d| 	d¡› �S )	zGLocal-disk equivalent of upload_to_s3, used when STORAGE_BACKEND=local.TrF   r}   zFailed to save zip locally: r6   Nz/files/r?   )
r   r=   rM  ÚmkdirÚshutilÚcopyfileÚOSErrorr   r>   r–  )rƒ  r…  ru  rÇ   r1   r1   r2   Úsave_to_local{  s   €ÿr�  Úcallback_urlÚpayloadc           	      C   s.  |  d¡}|  d¡}|r|st d¡ dS |› d|› �}t t d¡| d¡tj¡ 	¡ }dd|› �d	œ}t 
d
| › d|› d|› d|  dd¡› �¡ z.tj| ||td�}t 
d| › d|j› �¡ |jsut d| › d|jdd… › �¡ W dS W dS  tjy– } zt d| › d|› �¡ W Y d}~dS d}~ww )a  POST the final result to the resolved Laravel endpoint, with an
    HMAC-signed Bearer token. Best-effort: logs a warning on failure instead
    of raising, since we're already inside a fire-and-forget background task
    at this point. The token itself is never logged.re  rr  zECannot send callback: missing pharmacy_id or csv_filename in payload.Nú|rL   zapplication/jsonzBearer )zContent-TypeÚAuthorizationzSending callback -> z | pharmacy_id=ú csv_filename=z status=rˆ   Úsuccess)Újsonru   rx  zCallback response <- z | http_status=zCallback non-OK response from ú: r}   zCallback request FAILED to )r[   rw   rx   ÚhmacÚnewÚAWS_FILE_CONVERT_API_KEYÚencodeÚhashlibÚsha256Ú	hexdigestr  r{  ÚpostrS   r7   Úokr  r”   r  )	rž  rŸ  re  ÚfilenameÚdataÚtokenru   ÚresprÇ   r1   r1   r2   Úsend_callback†  sL   


ýüþÿ
ÿÿü&ÿ$€ÿr³  re  rr  rs  c                 C   s´  t |ƒ}ttjdd�ƒ}d|› d|› d|› �}t d|› �¡ dtt ¡ ƒ|||dœ}�z%z¬|d	 }	|d
 }
|
 ¡  t	| |	ƒ t d|	 
¡ j› d|› �¡ t|	|
ƒ}|sst d|› �¡ t|i |¥dddœ¥ƒ W W tj|dd� dS t dt|ƒ› d|› �¡ t| ƒ}|t|ƒj }t||ƒ t dt|ƒ› d|j› d|› �¡ tdkr«t||ƒ}ntr»tdkr»d}t|t||ƒ}nt||ƒ}t d|› d|› �¡ t||ƒ t d|› �¡ W nZ t�y } z%t d|j› d|j› d |› �¡ t|i |¥dt|jƒdœ¥ƒ W Y d}~n7d}~w t�y3 } zt d!|› �¡ t|i |¥dt|ƒdœ¥ƒ W Y d}~nd}~ww W tj|dd� dS W tj|dd� dS W tj|dd� dS tj|dd� w )"a*  Runs the actual CSV -> PDFs -> zip -> upload pipeline. Called via
    BackgroundTasks so /generate-report can return immediately. Always ends
    by POSTing a result (success or failure) to the Laravel URL resolved
    from receive_form (local/stage -> STAGE_LARAVEL_URL, live -> LIVE_LARAVEL_URL).Ú
cd_report_©Úprefixúpharmacy_id=r¢  ú env=z+[generate-report] BACKGROUND JOB STARTED | ú
csv-to-pdf©Úrequest_typer—   re  Úresult_filenamerr  ú	input.csvÚpdfsz"[generate-report] CSV downloaded (ú
 bytes) | z3[generate-report] No PDFs generated (empty CSV?) | Úfailedú2No PDFs were generated from the CSV (empty file?).©rˆ   r  T©Úignore_errorsNz[generate-report] Generated z
 PDF(s) | z[generate-report] Zipped z PDF(s) -> ú | rU   r<   rˆ  z[generate-report] Zip saved -> z7[generate-report] BACKGROUND JOB COMPLETED (success) | z7[generate-report] BACKGROUND JOB FAILED (HTTPException r¥  ú) | z=[generate-report] BACKGROUND JOB FAILED (unexpected error) | )r_   r   ÚtempfileÚmkdtemprw   r  ÚintÚtimer™  r‚  ÚstatÚst_sizer;  r  r³  rš  Úrmtreer¶   rl  rf   rD  r;   r�  r#   r˜  r   r7   r8   r‰   rÄ   Ú	exception)r�   rX   re  rr  rs  rž  Úwork_dirÚlog_ctxÚbase_payloadr±   Úpdf_dirr(  Úzip_keyr<  Úzip_urlr†  rÇ   r1   r1   r2   Úprocess_report_background·  s„   
û

ÿ
ý&Ü
"

 ÿý€ÿý€þ÷óú"rÕ  Úpdf_filenamec                 C   sö  t |ƒ}ttjdd�ƒ}d|› d|› d|› �}t d|› �¡ dtt ¡ ƒ|||dœ}�zFzÍ|d	 }	|d
 }
|
 ¡  t	| |	ƒ t d|	 
¡ j› d|› �¡ tj |¡d }t|	|
|ƒ}|s|t d|› �¡ t|i |¥dddœ¥ƒ W W tj|dd� dS t dt|ƒ› d|› �¡ t|ƒdkrœ|d }t| ƒ}|}d}n$|› d�}|| }t||ƒ t| ƒ}d}t dt|ƒ› d|› d|› �¡ ||d< tdkrÎt||ƒ}ntrÜtd krÜt|t||ƒ}nt||ƒ}t d!|› d|› �¡ t||ƒ t d"|› �¡ W nZ t�y, } z%t d#|j› d$|j› d%|› �¡ t|i |¥dt|jƒdœ¥ƒ W Y d}~n7d}~w t �yT } zt !d&|› �¡ t|i |¥dt|ƒdœ¥ƒ W Y d}~nd}~ww W tj|dd� dS W tj|dd� dS W tj|dd� dS tj|dd� w )'uP  Runs the CSV -> PDF(s) -> upload pipeline for
    /generate-csv-to-pdf-report. Same background-task / callback-routing
    pattern as process_report_background(), but produces one combined PDF
    via generate_single_pdf() â€” UNLESS the CSV has more than
    MAX_ROWS_PER_PDF rows, in which case generate_single_pdf() returns
    multiple "_PartN" PDFs and this function bundles them into a zip instead
    (same OOM-avoidance reasoning as process_report_background). Either way,
    the callback payload's 'result_filename' points at whichever file
    actually got uploaded (.pdf or .zip).Úcsv_to_pdf_rµ  r·  r¢  r¸  z6[generate-csv-to-pdf-report] BACKGROUND JOB STARTED | r¹  rº  r½  r¾  z-[generate-csv-to-pdf-report] CSV downloaded (r¿  r   z=[generate-csv-to-pdf-report] No PDF generated (empty CSV?) | rÀ  ú0No PDF was generated from the CSV (empty file?).rÂ  TrÃ  Nz.[generate-csv-to-pdf-report] PDF generated -> z part(s) | rÓ   rŒ  rn  rˆ  zB[generate-csv-to-pdf-report] CSV too large for one PDF -> bundled ú parts into rÅ  r¼  rU   r<   z-[generate-csv-to-pdf-report] Result saved -> zB[generate-csv-to-pdf-report] BACKGROUND JOB COMPLETED (success) | zB[generate-csv-to-pdf-report] BACKGROUND JOB FAILED (HTTPException r¥  rÆ  zH[generate-csv-to-pdf-report] BACKGROUND JOB FAILED (unexpected error) | )"r_   r   rÇ  rÈ  rw   r  rÉ  rÊ  r™  r‚  rË  rÌ  ro  rg  rq  r]  r  r³  rš  rÍ  r¶   rm  rD  rl  r;   r�  r#   r˜  r   r7   r8   r‰   rÄ   rÎ  )r�   rX   re  rr  rÖ  rž  rÏ  rÐ  rÑ  r±   rÒ  Ú	base_namer(  Úresult_pathÚ
result_keyr¼  r†  Ú
result_urlrÇ   r1   r1   r2   Úprocess_csv_to_pdf_background	  s¤   
û
ÿ
ý4Î

ÿÿÿÿ

 ÿý€ÿý€þ÷óú"rÞ  z/generate-report)Úresponse_modelÚdependenciesÚreqÚbackground_tasksc           
      C   sÞ   t | jƒ}|jr|jjnd}t d|› d|› �¡ z
t|ƒ\}}}}W n  tyC }	 zt d|› d|› d|	j	› d|	j
› �¡ ‚ d}	~	ww t d|› d	|› d
|› d|› �¡ | t|||||¡ t d|› d
|› �¡ tdd�S )a;  Validates csv_url, kicks off the CSV -> PDF -> zip -> upload pipeline
    in the background, and returns immediately. Once the background job
    finishes (success or failure), the result is POSTed to whichever Laravel
    endpoint matches the csv_url's environment segment (local/stage -> stage,
    live -> live).Úunknownz([generate-report] REQUEST RECEIVED from ú | csv_url=z([generate-report] REQUEST REJECTED from rÅ  r¥  Nz*[generate-report] REQUEST VALIDATED | env=ú pharmacy_id=r¢  z zip_filename=z6[generate-report] Background job queued | pharmacy_id=r£  ©rˆ   )r‰   r�   r”  Úhostrw   r  rt  r   r  r7   r8   Úadd_taskrÕ  r‡   )
rá  râ  rn   r�   Ú	client_iprX   re  rr  rs  rÇ   r1   r1   r2   Úgenerate_reports  s>   
ÿÿ€ýÿÿÿú	
rê  z/generate-csv-to-pdf-reportc                 C   sô   t | jƒ}|jr|jjnd}t d|› d|› �¡ z
t|ƒ\}}}}W n  tyC }	 zt d|› d|› d|	j	› d|	j
› �¡ ‚ d}	~	ww tj |¡d › d	�}
t d
|› d|› d|› d|
› �¡ | t|||||
¡ t d|› d|› �¡ tdd�S )u]  Same request/response/callback-routing contract as /generate-report,
    but converts the whole CSV into ONE combined landscape PDF (via
    generate_single_pdf()) instead of one PDF per drug + a zip â€” unless the
    CSV is large enough to need splitting (see MAX_ROWS_PER_PDF), in which
    case the result is bundled into a zip instead, same as /generate-report.
    The result is stored alongside the CSV in the same report_files/ S3 (or
    local) folder.

    Request body is identical to /generate-report:
      {"csv_url": "https://.../local/pharmacies/<id>/report_files/<name>.csv"}

    Returns IMMEDIATELY: {"status": "success"}

    Once the background job finishes, the result is POSTed to whichever
    Laravel endpoint matches the csv_url's environment segment (same
    local/stage -> stage, live -> live routing as /generate-report):

      {
        "request_type": "csv-to-pdf",
        "timestamp": 1784021199,
        "pharmacy_id": "N0tZbTJJRTFSYVhHanpGSm1EbVFKdz09",
        "result_filename": "6a38ce059932a-cd-entries.pdf",
        "csv_filename": "6a38ce059932a-cd-entries.csv"
      }
    rã  z3[generate-csv-to-pdf-report] REQUEST RECEIVED from rä  z3[generate-csv-to-pdf-report] REQUEST REJECTED from rÅ  r¥  Nr   rå   z5[generate-csv-to-pdf-report] REQUEST VALIDATED | env=rå  r¢  z pdf_filename=zA[generate-csv-to-pdf-report] Background job queued | pharmacy_id=r£  ræ  )r‰   r�   r”  rç  rw   r  rt  r   r  r7   r8   ro  rg  rq  rè  rÞ  r‡   )rá  râ  rn   r�   ré  rX   re  rr  Ú_zip_filenamerÇ   rÖ  r1   r1   r2   Úgenerate_csv_to_pdf_report›  s@   
ÿÿ€ýÿÿÿú	
rì  z/generate-report/download)rà  c              
   C   s¬  |j r|j jnd}t d|› d| › �¡ ttjdd�ƒ}|d }|d }| ¡  zrtt	| ƒ|ƒ t d| 
¡ j› d	| › �¡ t||ƒ}|sSt d
| › �¡ tddd�‚t dt|ƒ› d| › �¡ |sytj tt	| ƒƒj¡}tj |¡d › d�}n| ¡  d¡s…|› d�}|| }	t||	ƒ t d|› d| › �¡ W n2 ty©   tj|dd� ‚  tyÌ }
 zt d| › �¡ tj|dd� tdd|
› �d�‚d}
~
ww tt	|	ƒ|dd�S )uª  Same pipeline, but streams the zip back directly instead of uploading to S3.
    Useful for local testing without AWS credentials. This endpoint remains
    synchronous (no background task, no callback) since the point is to get
    the file back in the same request.

    zip_filename is OPTIONAL â€” if omitted, it's derived automatically from
    csv_url, e.g. ".../2906266-cd-entries.csv" -> "2906266-cd-entries.zip".
    rã  z1[generate-report/download] REQUEST RECEIVED from rä  Úcd_report_dl_rµ  r½  r¾  z+[generate-report/download] CSV downloaded (ú bytes) | csv_url=zD[generate-report/download] No PDFs generated (empty CSV?) | csv_url=rY   rÁ  r6   z%[generate-report/download] Generated z PDF(s) | csv_url=r   rn  z;[generate-report/download] Streaming zip back to client -> TrÃ  zG[generate-report/download] REQUEST FAILED (unexpected error) | csv_url=r}   úFailed to generate report: NrŠ  ©rg  r¯  Ú
media_type)r”  rç  rw   r  r   rÇ  rÈ  r™  r‚  r‰   rË  rÌ  r;  r  r   r¶   ro  rg  rp  r   rq  r\   ÚendswithrD  rš  rÍ  rÄ   rÎ  r   )r�   rn   rs  ré  rÏ  r±   rÒ  r(  rr  r<  rÇ   r1   r1   r2   Úgenerate_report_downloadÚ  sH   



€ýýró  z$/generate-csv-to-pdf-report/downloadc              
   C   sð  |j r|j jnd}t d|› d| › �¡ ttjdd�ƒ}|d }|d }| ¡  z”tt	| ƒ|ƒ t d| 
¡ j› d	| › �¡ |sWtj tt	| ƒƒj¡}tj |¡d
 › d�}n| ¡  d¡sc|› d�}tj |¡d
 }t|||ƒ}	|	s�t d| › �¡ tddd�‚t|	ƒdkr�|	d
 }
|}d}n |› d�}|| }
t|	|
ƒ d}t dt|	ƒ› d|› d| › �¡ t d|› d| › �¡ W n2 tyË   tj|dd� ‚  tyî } zt d| › �¡ tj|dd� tdd|› �d�‚d}~ww tt	|
ƒ||d�S )uR  Same pipeline as /generate-csv-to-pdf-report, but streams the result
    back directly instead of uploading to S3 â€” the PDF equivalent of
    /generate-report/download. Returns a single .pdf for CSVs up to
    MAX_ROWS_PER_PDF rows, or a .zip of "_PartN" PDFs for larger ones (same
    fallback as the background version). Synchronous (no background task,
    no callback) since the point is to get the file back in the same request.

    pdf_filename is OPTIONAL â€” if omitted, it's derived automatically from
    csv_url, e.g. ".../2906266-cd-entries.csv" -> "2906266-cd-entries.pdf".
    rã  z<[generate-csv-to-pdf-report/download] REQUEST RECEIVED from rä  Úcsv_to_pdf_dl_rµ  r½  r¾  z6[generate-csv-to-pdf-report/download] CSV downloaded (rî  r   rå   zN[generate-csv-to-pdf-report/download] No PDF generated (empty CSV?) | csv_url=rY   rØ  r6   rÓ   r�  rn  rŠ  zK[generate-csv-to-pdf-report/download] CSV too large for one PDF -> bundled rÙ  zI[generate-csv-to-pdf-report/download] Streaming result back to client -> TrÃ  zR[generate-csv-to-pdf-report/download] REQUEST FAILED (unexpected error) | csv_url=r}   rï  Nrð  )r”  rç  rw   r  r   rÇ  rÈ  r™  r‚  r‰   rË  rÌ  ro  rg  rp  r   rq  r\   rò  r]  r  r   r¶   rD  rš  rÍ  rÄ   rÎ  r   )r�   rn   rÖ  ré  rÏ  r±   rÒ  rr  rÚ  r(  rÛ  r¼  rñ  rÇ   r1   r1   r2   Ú#generate_csv_to_pdf_report_download  sf   


ÿÿÿÿ€ýýrõ  z/healthc                   C   s   t ddiƒS )Nrˆ   r®  )r   r1   r1   r1   r2   ÚhealthZ  s   rö  rµ   )–Ú__doc__r"  rª  r¦  Úloggingr  ro  r�   r-   rš  rÇ  rÊ  r>  Úhtmlr   Úlogging.handlersr   Úpathlibr   r   Útypingr   Úurllib.parser   r“  Úpandasr½   r{  Úbotocore.exceptionsr   r	   Údotenvr
   Úfastapir   r   r   Úfastapi.responsesr   r   Úfastapi.staticfilesr   Úpydanticr   r   Úreportlab.libr   Úreportlab.lib.enumsr   Úreportlab.lib.pagesizesr   r   r   Úreportlab.lib.stylesr   Úreportlab.platypusr   r   r   r   r   r   r   r    Úfastapi.securityr!   Úenvironr[   r"   r#   r%   r&   r'   rÉ  r(   r)   r*   r+   Úfloatr3   r‰   r:   rg   r\   r;   r=   rh   r>   r@   ÚupperrA   rC   rD   rM  r™  Ú	getLoggerrw   ÚsetLevelÚ	propagateÚ	FormatterÚ_log_formatterÚhandlersÚ_file_handlerÚsetFormatterÚ
addHandlerÚStreamHandlerÚ_console_handlerrP   rQ   r¨  rS   rZ   r_   r  ÚappÚmountÚsplitrl   ry   rz   Úapi_key_headerr~   r€   r‡   r�   r•   r°   Ú	DataFramerÊ   rÃ   r;  rD  r]  rl  rm  Útuplert  r‚  r˜  r�  Údictr³  rÕ  rÞ  r­  rê  rì  ró  rõ  rö  r1   r1   r1   r2   Ú<module>   s<    þþ	

þÿ



þþý
þ&/ y"1ÿþýüû
úRÿþýüû
új'>8F